Compare commits

..

1 Commits

Author SHA1 Message Date
58b363b8dc ci: Instrumentation tests coverage in ci.
Some checks failed
Check PR code / EspressoReports (pull_request) Has been cancelled
2025-03-20 09:45:20 +01:00
35 changed files with 465 additions and 797 deletions

View File

@ -6,18 +6,16 @@ jobs:
BuildAndTestAndCoverage: BuildAndTestAndCoverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Check out repository code
uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: "Check android app changes" - name: "Check android app changes"
id: check-android-changes id: check-android-changes
uses: tj-actions/changed-files@v46 uses: tj-actions/changed-files@v45
with: with:
files: | files: |
androidApp/src/** androidApp/src/**
shared/src/commonMain/**
shared/src/androidMain/**
shared/src/commonTest/**
- name: Fetch tags - name: Fetch tags
if: steps.check-android-changes.outputs.any_modified == 'true' if: steps.check-android-changes.outputs.any_modified == 'true'
run: git fetch --tags -p run: git fetch --tags -p
@ -26,6 +24,7 @@ jobs:
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v3
if: steps.check-android-changes.outputs.any_modified == 'true' if: steps.check-android-changes.outputs.any_modified == 'true'
- uses: android-actions/setup-android@v3 - uses: android-actions/setup-android@v3
@ -35,7 +34,14 @@ jobs:
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test - name: Build and test
if: steps.check-android-changes.outputs.any_modified == 'true' if: steps.check-android-changes.outputs.any_modified == 'true'
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
# TESTS ARE RUN LOCALLY
# - 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
- name: coverage - name: coverage
if: steps.check-android-changes.outputs.any_modified == 'true' if: steps.check-android-changes.outputs.any_modified == 'true'
run: | run: |
@ -48,3 +54,8 @@ jobs:
retention-days: 1 retention-days: 1
overwrite: true overwrite: true
include-hidden-files: true include-hidden-files: true
# TESTS ARE RUN LOCALLY
# - name: Clean
# if: always()
# run: |
# docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -1,11 +1,9 @@
name: PR test name: Coverage
on: on:
pull_request: workflow_call:
branches:
- master
jobs: jobs:
integrationTests: BuildAndTestAndCoverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository code - name: Check out repository code
@ -14,50 +12,38 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Fetch tags - name: Fetch tags
run: git fetch --tags -p run: git fetch --tags -p
- 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
- uses: KengoTODA/actions-setup-docker-compose@v1 - uses: KengoTODA/actions-setup-docker-compose@v1
with: with:
version: "2.23.3" version: "2.23.3"
- name: run selfoss - name: run selfoss
run: | run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- 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: Change url until I find a better way to do it
run: |
sed -i "s/const val DEFAULT_URL = \"http:\/\/10\.0\.2\.2\:8888\"/const val DEFAULT_URL = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt
- name: Tests - name: Tests
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: 29 api-level: 29
profile: pixel_2 script: ./gradlew androidApp:connectedAndroidTest
script: |
./gradlew androidApp:clearScreenshotsTask || true
./gradlew androidApp:createScreenshotDirectory
adb logcat -G 16M
./gradlew JacocoDebugCodeCoverage || (./gradlew androidApp:fetchScreenshots && adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt)
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure()
with: with:
name: screenshot-espresso name: failure-espresso
path: androidApp/build/reports/androidTests/connected/screenshots path: build/reports/androidTests/connected/screenshots
retention-days: 2 retention-days: 2
overwrite: true overwrite: true
include-hidden-files: true include-hidden-files: true
- uses: actions/upload-artifact@v3
with:
path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig
retention-days: 1
overwrite: true
include-hidden-files: true
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: coverage-espresso name: coverage-espresso
path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage path: build/reports/coverage/androidTest/githubConfig/debug/connected
retention-days: 1 retention-days: 1
overwrite: true overwrite: true
include-hidden-files: true include-hidden-files: true

View File

@ -1,4 +1,4 @@
name: Realease name: Create tag
on: on:
push: push:
branches: branches:
@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
uses: ./.gitea/workflows/on_called_build.yml uses: ./.gitea/workflows/common_build.yml
createTagAndChangelog: createTagAndChangelog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: build
@ -86,6 +86,7 @@ jobs:
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Configure gradle... - name: Configure gradle...

View File

@ -1,90 +1,93 @@
name: PR name: Check PR code
on: on:
pull_request: pull_request:
branches: branches:
- master - master
jobs: jobs:
PR: EspressoReports:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: uses: ./.gitea/workflows/common_coverage.yml
- name: Check out repository code # Lint:
uses: actions/checkout@v4 # runs-on: ubuntu-latest
- uses: actions/setup-java@v4 # steps:
with: # - name: Check out repository code
distribution: 'temurin' # uses: actions/checkout@v4
java-version: '17' # - uses: actions/setup-java@v4
- name: Install klint # with:
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ # distribution: 'temurin'
- name: Install detekt # java-version: '17'
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip # cache: gradle
- name: Linting... # - name: Install klint
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' # run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- name: Detecting... # - name: Install detekt
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' # run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip
translations: # - name: Linting...
runs-on: ubuntu-latest # run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
steps: # - name: Detecting...
- name: Check out repository code # run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
uses: actions/checkout@v4 # translations:
with: # runs-on: ubuntu-latest
fetch-depth: 0 # steps:
- name: "Check translations changes" # - name: Check out repository code
id: check-translations-changes # uses: actions/checkout@v4
uses: tj-actions/changed-files@v46 # with:
with: # fetch-depth: 0
base_sha: ${{ github.event.pull_request.base.sha }} # - name: "Check translations changes"
files: | # id: check-translations-changes
androidApp/src/main/res/values/strings.xml # uses: tj-actions/changed-files@v45
- name: upload translation sources # with:
if: steps.check-translations-changes.outputs.any_modified == 'true' # files: |
uses: crowdin/github-action@v2 # androidApp/src/main/res/values/strings.xml
with: # - name: upload translation sources
config: './.gitea/workflows/assets/crowdin.yml' # if: steps.check-api-changes.outputs.any_modified == 'true'
upload_sources: true # uses: crowdin/github-action@v2
upload_translations: false # with:
download_translations: false # config: './.gitea/workflows/assets/crowdin.yml'
create_pull_request: false # upload_sources: true
push_translations: false # upload_translations: false
env: # download_translations: false
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} # create_pull_request: false
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} # push_translations: false
- name: wait # env:
if: steps.check-translations-changes.outputs.any_modified == 'true' # CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
run: sleep 10s # CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: download translations # - name: wait
if: steps.check-translations-changes.outputs.any_modified == 'true' # if: steps.check-api-changes.outputs.any_modified == 'true'
uses: crowdin/github-action@v2 # run: sleep 10s
with: # - name: download translations
config: './.gitea/workflows/assets/crowdin.yml' # if: steps.check-api-changes.outputs.any_modified == 'true'
upload_sources: false # uses: crowdin/github-action@v2
upload_translations: false # with:
download_translations: true # config: './.gitea/workflows/assets/crowdin.yml'
create_pull_request: false # upload_sources: false
push_translations: false # upload_translations: false
env: # download_translations: true
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} # create_pull_request: false
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} # push_translations: false
- name: Check for uncommitted changes # env:
if: steps.check-translations-changes.outputs.any_modified == 'true' # CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
id: check-changes # CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
uses: mskri/check-uncommitted-changes-action@v1.0.1 # - name: Check for uncommitted changes
- name: Commit Changes # if: steps.check-api-changes.outputs.any_modified == 'true'
if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' # id: check-changes
run: | # uses: mskri/check-uncommitted-changes-action@v1.0.1
git config --global user.email aminecmi+giteadrone@pm.me # - name: Commit Changes
git config --global user.name giteadrone # if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
git add ./androidApp/src/main/res/* # run: |
git commit -m "translation: translation files" # git config --global user.email aminecmi+giteadrone@pm.me
- name: Push changes # git config --global user.name giteadrone
if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' # git add ./androidApp/src/main/res/*
uses: appleboy/git-push-action@v1.0.0 # git commit -m "translation: translation files"
with: # - name: Push changes
author_name: giteadrone # if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
author_email: aminecmi+giteadrone@pm.me # uses: appleboy/git-push-action@v1.0.0
remote: ${{ secrets.REMOTE_URL }} # with:
ssh_key: ${{ secrets.PRIVATE_KEY }} # author_name: giteadrone
branch: ${{ github.head_ref || github.ref_name }} # author_email: aminecmi+giteadrone@pm.me
build: # remote: ${{ secrets.REMOTE_URL }}
needs: Lint # ssh_key: ${{ secrets.PRIVATE_KEY }}
uses: ./.gitea/workflows/on_called_build.yml # branch: ${{ github.head_ref || github.ref_name }}
# build:
# needs: Lint
# uses: ./.gitea/workflows/common_build.yml

View File

@ -1,4 +1,4 @@
name: Master name: Check master code
on: on:
push: push:
branches: branches:
@ -6,4 +6,4 @@ on:
jobs: jobs:
build: build:
uses: ./.gitea/workflows/on_called_build.yml uses: ./.gitea/workflows/common_build.yml

View File

@ -1,30 +1,3 @@
**v125040991
- fix: Connectivity toast message was causing issues.
- Changelog for v125030901
--------------------------------------------------------------------
**v125030901
- Merge pull request 'fix-reload' (#195) from fix-reload into master
- fix: Infinite scroll needs loading stats.
- fix: do not reload items on resume.
- Merge pull request 'tests' (#193) from tests into master
- ci: Instrumentation tests coverage in ci.
- ci: Instrumentation tests coverage in ci.
- ci: Instrumentation tests coverage in ci.
- chore: better handling of coroutine dispatchers.
- ci: Instrumentation tests coverage in ci.
- chore: comment robolectric tests for now.
- fix: Fixed source deletion test.
- Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
- Refactor star icon handling
- Don't restart activity changing alignment
- Changelog for v125030711
--------------------------------------------------------------------
**v125030711 **v125030711
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master - Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master

View File

@ -10,7 +10,6 @@ plugins {
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover") id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2" id("app.cash.sqldelight") version "2.0.2"
jacoco
} }
fun Project.execWithOutput( fun Project.execWithOutput(
@ -65,15 +64,6 @@ fun versionNameFromGit(): String {
return gitVersion() return gitVersion()
} }
val exclusions =
listOf(
"**/R.class",
"**/R\$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
)
android { android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
@ -105,6 +95,7 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
testInstrumentationRunnerArguments["useTestStorageService"] = "true" testInstrumentationRunnerArguments["useTestStorageService"] = "true"
} }
packaging { packaging {
@ -124,39 +115,6 @@ android {
installation { installation {
installOptions("-g", "-r") installOptions("-g", "-r")
} }
val androidTests = "connectedAndroidTest"
tasks.register<JacocoReport>("JacocoDebugCodeCoverage") {
// Depend on unit tests and Android tests tasks
dependsOn(listOf(androidTests))
// Set task grouping and description
group = "Reporting"
description = "Execute UI and unit tests, generate and combine Jacoco coverage report"
// Configure reports to generate both XML and HTML formats
reports {
xml.required.set(true)
html.required.set(true)
}
// Set source directories to the main source directory
sourceDirectories.setFrom(layout.projectDirectory.dir("src/main"))
// Set class directories to compiled Java and Kotlin classes, excluding specified exclusions
classDirectories.setFrom(
files(
fileTree(layout.buildDirectory.dir("intermediates/javac/")) {
exclude(exclusions)
},
fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) {
exclude(exclusions)
},
),
)
// Collect execution data from .exec and .ec files generated during test execution
executionData.setFrom(
files(
fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) },
),
)
}
} }
} }
flavorDimensions.add("build") flavorDimensions.add("build")
@ -169,6 +127,7 @@ 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
} }
@ -201,8 +160,8 @@ dependencies {
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
// About // About
implementation("com.mikepenz:aboutlibraries-core:11.6.3") implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:11.6.3") implementation("com.mikepenz:aboutlibraries:10.5.1")
// Material-ish things // Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
@ -249,6 +208,7 @@ 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")
@ -272,12 +232,6 @@ tasks.withType<Test> {
) )
showStandardStreams = true showStandardStreams = true
} }
if (this.name == "connectedAndroidTest") {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}
} }
aboutLibraries { aboutLibraries {
@ -291,29 +245,39 @@ aboutLibraries {
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
} }
// Screenshot failure handling
val reportsDirectory = file("$buildDir/reports/androidTests/connected")
val clearScreenshotsTask = val clearScreenshotsTask =
tasks.register<Exec>("clearScreenshots") { tasks.register<Exec>("clearScreenshots") {
println("AMINE : clear") println("AMINE : clear")
commandLine = listOf("adb", "shell", "rm", "-r", "/storage/emulated/0/Pictures/selfoss_tests/screenshots/*") commandLine = listOf("adb", "shell", "rm", "-r", "/sdcard/Pictures/selfoss_tests")
} }
val createScreenshotDirectoryTask = val createScreenshotDirectoryTask =
tasks.register<Exec>("createScreenshotDirectory") { tasks.register<Exec>("createScreenshotDirectory") {
println("AMINE : create directory") println("AMINE : create directory")
group = "reporting" group = "reporting"
commandLine = listOf("adb", "shell", "mkdir", "-p", "/storage/emulated/0/Pictures/selfoss_tests/screenshots") commandLine = listOf("adb", "shell", "mkdir", "-p", "/sdcard/Pictures/selfoss_tests")
} }
tasks.register<Exec>("fetchScreenshots") { val fetchScreenshotsTask =
val reportsDirectory = file("$buildDir/reports/androidTests/connected") tasks.register<Exec>("fetchScreenshots") {
println("AMINE : fetch") println("AMINE : fetch")
group = "reporting" group = "reporting"
executable(android.adbExecutable.toString()) executable(android.adbExecutable.toString())
commandLine = listOf("adb", "pull", "/storage/emulated/0/Pictures/selfoss_tests/screenshots", reportsDirectory.toString()) commandLine = listOf("adb", "pull", "/sdcard/Pictures/selfoss_tests/.", reportsDirectory.toString())
finalizedBy(clearScreenshotsTask) finalizedBy(clearScreenshotsTask)
dependsOn(createScreenshotDirectoryTask)
doFirst { doFirst {
reportsDirectory.mkdirs() reportsDirectory.mkdirs()
}
}
tasks.whenTaskAdded {
if (this.name == "connectedAndroidTest") {
this.finalizedBy(fetchScreenshotsTask)
} }
} }

View File

@ -1,7 +1,6 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.os.Environment.DIRECTORY_PICTURES import android.os.Environment.DIRECTORY_PICTURES
import android.os.Environment.getExternalStoragePublicDirectory import android.os.Environment.getExternalStoragePublicDirectory
import android.util.Log import android.util.Log
@ -15,38 +14,43 @@ import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.base.DefaultFailureHandler import androidx.test.espresso.base.DefaultFailureHandler
import androidx.test.espresso.matcher.RootMatchers.isDialog
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.isNotChecked import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor
import androidx.test.runner.screenshot.Screenshot
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers.hasToString import org.hamcrest.Matchers.hasToString
import org.junit.BeforeClass import org.junit.BeforeClass
import java.io.BufferedOutputStream import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.Locale import java.util.Locale
// For now, do not move this as it is modified by the integration tests val defaultUrl = (System.getenv("SELFOSS_URL") ?: "").ifEmpty { "http://10.0.2.2:8888" }
const val DEFAULT_URL = "http://10.0.2.2:8888"
fun performLogin(someUrl: String? = null) { fun performLogin(someUrl: String? = null) {
Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL}") Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl}")
onView(withId(R.id.urlView)).perform(click()).perform( onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView( typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL, if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl,
), ),
) )
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,
@ -112,12 +116,6 @@ 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,
@ -141,11 +139,6 @@ fun testAddSourceWithUrl(
onView(withText(sourceName)).check(matches(isDisplayed())) onView(withText(sourceName)).check(matches(isDisplayed()))
} }
fun checkHomeLoadingDone() {
onView(withId(R.id.swipeRefreshLayout)).inRoot(not(isDialog())).perform(waitForRecyclerViewToStopLoading(300000))
}
@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.
@ -160,7 +153,6 @@ open class WithANRException {
"default root matcher, it may be picking a root that never takes focus. " + "default root matcher, it may be picking a root that never takes focus. " +
"Root:", "Root:",
) )
private const val OTHER_EXCEPTION = "System Ul isn't responding"
private fun handleAnrDialogue() { private fun handleAnrDialogue() {
val device = UiDevice.getInstance(getInstrumentation()) val device = UiDevice.getInstance(getInstrumentation())
@ -174,17 +166,10 @@ open class WithANRException {
fun setUpHandler() { fun setUpHandler() {
Espresso.setFailureHandler { error, viewMatcher -> Espresso.setFailureHandler { error, viewMatcher ->
takeScreenshot() if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) && anrCount < 3) {
if (error.message!!.contains(OTHER_EXCEPTION)) {
handleAnrDialogue()
} else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) &&
anrCount < 20
) {
anrCount++ anrCount++
handleAnrDialogue() handleAnrDialogue()
} else { // chain all failures down to the default espresso handler } else { // chain all failures down to the default espresso handler
Log.e("AMINE", "AMINE : ${error.message}")
println("AMINE : ${error.message}")
DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher) DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
} }
} }
@ -192,41 +177,47 @@ open class WithANRException {
} }
} }
@Suppress("detekt:NestedBlockDepth") class MyScreenCaptureProcessor(
fun takeScreenshot() { parentFolderPath: String,
try { ) : BasicScreenCaptureProcessor() {
val bitmap = getInstrumentation().uiAutomation.takeScreenshot() init {
this.mDefaultScreenshotPath =
val folder =
File( File(
File( File(
getExternalStoragePublicDirectory(DIRECTORY_PICTURES), getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
"selfoss_tests", "selfoss_tests",
).absolutePath, ).absolutePath,
"screenshots", "screenshots/$parentFolderPath",
) )
if (!folder.exists()) { }
folder.mkdirs()
}
var out: BufferedOutputStream? = null override fun getFilename(prefix: String): String = prefix
val size = folder.list().size + 1 }
try {
out = BufferedOutputStream(FileOutputStream(folder.path + "/" + size + ".png")) fun takeScreenshot(
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) parentFolderPath: String = "",
Log.d("Screenshots", "Screenshot taken") screenShotName: String,
} catch (e: IOException) { ) {
Log.e("Screenshots", "Could not save the screenshot", e) Log.d("Screenshots", "Taking screenshot of '$screenShotName'")
} finally { val screenCapture = Screenshot.capture()
if (out != null) { val processors = setOf(MyScreenCaptureProcessor(parentFolderPath))
try { try {
out.close() screenCapture.apply {
} catch (e: IOException) { name = screenShotName
Log.e("Screenshots", "Could not save the screenshot", e) process(processors)
}
}
} }
Log.d("Screenshots", "Screenshot taken")
} catch (ex: IOException) { } catch (ex: IOException) {
Log.e("Screenshots", "Could not take the screenshot", ex) Log.e("Screenshots", "Could not take the screenshot", ex)
} }
} }
class ScreenshotTakingRule : TestWatcher() {
override fun failed(
e: Throwable?,
description: Description,
) {
val parentFolderPath = "failures/${description.className}"
takeScreenshot(parentFolderPath = parentFolderPath, screenShotName = description.methodName)
}
}

View File

@ -8,32 +8,22 @@ import android.widget.RelativeLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.PerformException
import androidx.test.espresso.Root import androidx.test.espresso.Root
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withResourceName import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.any
import org.hamcrest.Description import org.hamcrest.Description
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher import org.hamcrest.TypeSafeMatcher
import java.util.concurrent.TimeoutException
fun withError( fun withError(
@StringRes id: Int, @StringRes id: Int,
@ -54,86 +44,6 @@ fun withError(
} }
} }
fun waitUntilShown(
viewText: String,
millis: Long,
): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = isRoot()
override fun getDescription(): String = "wait for $millis millis, for a specific view with text <$viewText> to be visible."
override fun perform(
uiController: UiController,
view: View,
) {
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + millis
val viewMatcher = withText(viewText)
do {
for (child in TreeIterables.breadthFirstViewTraversal(view)) {
if (viewMatcher.matches(child) && child.isShown) {
return
}
}
uiController.loopMainThreadForAtLeast(100)
} while (System.currentTimeMillis() < endTime)
// timeout happens
throw PerformException
.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))
.withCause(TimeoutException())
.build()
}
}
}
fun waitForRecyclerViewToStopLoading(millis: Long): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = any(View::class.java)
override fun getDescription(): String = "wait for $millis millis for the recyclerview to stop loading."
override fun perform(
uiController: UiController,
view: View?,
) {
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + millis
do {
// either the empty view is displayed
for (child in TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (withId(R.id.emptyText).matches(child) && child.isVisible) {
return
}
}
// or the refresh layout is refreshing
if (view is SwipeRefreshLayout && !view.isRefreshing) {
return
}
uiController.loopMainThreadForAtLeast(100)
} while (System.currentTimeMillis() < endTime)
// timeout happens
throw PerformException
.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))
.withCause(TimeoutException())
.build()
}
}
}
fun isPopupWindow(): Matcher<Root> = isPlatformPopup() fun isPopupWindow(): Matcher<Root> = isPlatformPopup()
fun withDrawable( fun withDrawable(

View File

@ -1,7 +1,6 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions 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
@ -15,34 +14,34 @@ 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.testing.CountingIdlingResourceSingleton
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.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 `2-HomeActivityTest` : WithANRException() { class HomeActivityTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(HomeActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
@Before @Before
fun registerIdlingResource() { fun init() {
IdlingRegistry loginAndInitHome()
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
checkHomeLoadingDone()
} }
@Test @Test
fun testMenu() { fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( onView(withId(R.id.action_search)).check(matches(not(isDisplayed()))).check(
matches( matches(
isClickable(), isClickable(),
), ),

View File

@ -15,20 +15,24 @@ 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.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 `1-LoginActivityTest` : WithANRException() { class LoginActivityTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
@Before @Before
fun registerIdlingResource() { fun registerIdlingResource() {
IdlingRegistry IdlingRegistry
@ -44,7 +48,7 @@ class `1-LoginActivityTest` : WithANRException() {
} }
@Test @Test
fun `1-viewIsInitialized`() { fun 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()))
@ -61,27 +65,28 @@ class `1-LoginActivityTest` : WithANRException() {
} }
@Test @Test
fun `2-urlError`() { fun 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 `3-urlSlashError`() { fun 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 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 `4-connectError`() { fun multiError() {
performLogin("http://10.0.2.2:8889")
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())
@ -89,10 +94,8 @@ class `1-LoginActivityTest` : WithANRException() {
} }
@Test @Test
fun `6-connect`() { fun 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())
checkHomeLoadingDone()
} }
} }

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android
import androidx.test.core.app.ApplicationProvider 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.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
@ -20,29 +19,30 @@ 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.testing.CountingIdlingResourceSingleton
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.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 `4-SettingsActivityGeneralTest` : WithANRException() { class SettingsActivityGeneralTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(HomeActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
@Before @Before
fun init() { fun init() {
IdlingRegistry loginAndInitHome()
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
) )

View File

@ -1,36 +1,39 @@
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.IdlingRegistry 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 bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
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.runner.RunWith import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class `6-SettingsActivityOfflineTest` : WithANRException() { class SettingsActivityOfflineTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(SettingsActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
lateinit var context: Context lateinit var context: Context
@ -39,17 +42,14 @@ class `6-SettingsActivityOfflineTest` : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context context = activity.window.context
} }
IdlingRegistry loginAndInitHome()
.getInstance() openActionBarOverflowOrOptionsMenu(
.register(CountingIdlingResourceSingleton.countingIdlingResource) 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

@ -1,34 +1,37 @@
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.IdlingRegistry 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 bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
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.runner.RunWith import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class `5-SettingsActivityReaderTest` : WithANRException() { class SettingsActivityReaderTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(SettingsActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
lateinit var context: Context lateinit var context: Context
@ -37,17 +40,14 @@ class `5-SettingsActivityReaderTest` : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context context = activity.window.context
} }
IdlingRegistry loginAndInitHome()
.getInstance() openActionBarOverflowOrOptionsMenu(
.register(CountingIdlingResourceSingleton.countingIdlingResource) 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

@ -2,30 +2,34 @@ package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
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.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.espresso.matcher.ViewMatchers.isSelected
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.testing.CountingIdlingResourceSingleton
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.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") class SettingsActivityTest : WithANRException() {
class `3-SettingsActivityTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(HomeActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
lateinit var context: Context lateinit var context: Context
@ -34,9 +38,7 @@ class `3-SettingsActivityTest` : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context context = activity.window.context
} }
IdlingRegistry loginAndInitHome()
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
openMenu() openMenu()
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
} }
@ -75,9 +77,6 @@ class `3-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
@ -97,7 +96,6 @@ class `3-SettingsActivityTest` : WithANRException() {
@Test @Test
fun testAbout() { fun testAbout() {
onView(withText(R.string.action_about)).perform(click()) onView(withText(R.string.action_about)).perform(click())
onView(isRoot()).perform(waitUntilShown("ACRA", 30000))
onView(withText("ACRA")).check(matches(isDisplayed())) onView(withText("ACRA")).check(matches(isDisplayed()))
} }
} }

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.AmbiguousViewMatcherException
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.swipeDown
@ -15,30 +14,34 @@ 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.testing.CountingIdlingResourceSingleton
import org.junit.After 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.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 `7-SourcesActivityTest` : WithANRException() { class SourcesActivityTest : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(HomeActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@JvmField
@Rule
val ruleChain: RuleChain =
RuleChain
.outerRule(activityRule)
.around(ScreenshotTakingRule())
lateinit var sourceName: String lateinit var sourceName: String
@Before @Before
fun init() { fun init() {
IdlingRegistry
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
sourceName = UUID.randomUUID().toString().substring(0, 15) sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome()
goToSources() goToSources()
} }
@ -80,4 +83,10 @@ class `7-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

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApp"
android:allowBackup="false"
android:configChanges="uiMode"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/NoBar"
tools:replace="android:allowBackup">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".LoginActivity"
android:label="@string/title_activity_login"></activity>
<activity android:name=".HomeActivity"></activity>
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" />
</activity>
<activity
android:name=".SourcesActivity"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" />
</activity>
<activity
android:name=".UpsertSourceActivity"
android:exported="true"
android:parentActivityName=".SourcesActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".ReaderActivity"></activity>
<activity
android:name=".ImageActivity"
android:theme="@style/Theme.AppCompat.ImageActivity"></activity>
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<meta-data
android:name="android.max_aspect"
android:value="2.1" />
</application>
</manifest>

View File

@ -104,7 +104,7 @@ class HomeActivity :
if (appSettingsService.isItemCachingEnabled()) { if (appSettingsService.isItemCachingEnabled()) {
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -120,9 +120,12 @@ class HomeActivity :
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false repository.offlineOverride = false
lastFetchDone = false lastFetchDone = false
items.clear() CountingIdlingResourceSingleton.increment()
getElementsAccordingToTab() CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = false getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
}
} }
val swipeDirs = val swipeDirs =
@ -286,7 +289,7 @@ class HomeActivity :
handleRecurringTask() handleRecurringTask()
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
repository.handleDBActions() repository.handleDBActions()
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -459,28 +462,19 @@ class HomeActivity :
appendResults: Boolean, appendResults: Boolean,
itemType: ItemType, itemType: ItemType,
) { ) {
@Suppress("detekt:ComplexCondition") CountingIdlingResourceSingleton.increment()
if ((appendResults && items.size > 0) || (!appendResults && items.size == 0)) { CoroutineScope(Dispatchers.Main).launch {
CountingIdlingResourceSingleton.increment()
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
CoroutineScope(Dispatchers.IO).launch { repository.displayedItems = itemType
repository.displayedItems = itemType items =
items = if (appendResults) {
if (appendResults) { repository.getOlderItems()
repository.getOlderItems() } else {
} else { repository.getNewerItems()
repository.getNewerItems()
}
CountingIdlingResourceSingleton.increment()
launch(Dispatchers.Main) {
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
CountingIdlingResourceSingleton.decrement()
} }
CountingIdlingResourceSingleton.decrement() binding.swipeRefreshLayout.isRefreshing = false
}
} else {
handleListResult() handleListResult()
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -540,10 +534,7 @@ class HomeActivity :
} }
private fun reloadBadges() { private fun reloadBadges() {
if (appSettingsService.isInfiniteLoadingEnabled() || if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
appSettingsService.isDisplayUnreadCountEnabled() ||
appSettingsService.isDisplayAllCountEnabled()
) {
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges() repository.reloadBadges()
@ -622,26 +613,22 @@ class HomeActivity :
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
CountingIdlingResourceSingleton.increment() if (updatedRemote) {
launch(Dispatchers.Main) { Toast
if (updatedRemote) { .makeText(
Toast this@HomeActivity,
.makeText( R.string.refresh_success_response,
this@HomeActivity, Toast.LENGTH_LONG,
R.string.refresh_success_response, ).show()
Toast.LENGTH_LONG, } else {
).show() Toast
} else { .makeText(
Toast this@HomeActivity,
.makeText( R.string.refresh_failer_message,
this@HomeActivity, Toast.LENGTH_SHORT,
R.string.refresh_failer_message, ).show()
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -652,33 +639,30 @@ class HomeActivity :
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) { if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
CountingIdlingResourceSingleton.increment()
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
CoroutineScope(Dispatchers.IO).launch { CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
CountingIdlingResourceSingleton.increment() if (success) {
launch(Dispatchers.Main) { Toast
if (success) { .makeText(
Toast this@HomeActivity,
.makeText( R.string.all_posts_read,
this@HomeActivity, Toast.LENGTH_SHORT,
R.string.all_posts_read, ).show()
Toast.LENGTH_SHORT, tabNewBadge.removeBadge()
).show()
tabNewBadge.removeBadge()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast Toast
.makeText( .makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_not_read, R.string.all_posts_not_read,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
).show() ).show()
}
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
} }
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
} }

View File

@ -108,7 +108,7 @@ class LoginActivity :
private fun goToMain() { private fun goToMain() {
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation() repository.updateApiInformation()
ACRA.errorReporter.putCustomData( ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION", "SELFOSS_API_VERSION",
@ -127,12 +127,8 @@ class LoginActivity :
binding.urlView.error = getString(R.string.wrong_infos) binding.urlView.error = getString(R.string.wrong_infos)
binding.loginView.error = getString(R.string.wrong_infos) binding.loginView.error = getString(R.string.wrong_infos)
binding.passwordView.error = getString(R.string.wrong_infos) binding.passwordView.error = getString(R.string.wrong_infos)
binding.urlView.requestFocus()
showProgress(false)
} }
@Suppress("detekt:LongMethod")
private fun attemptLogin() { private fun attemptLogin() {
// Reset errors. // Reset errors.
binding.urlView.error = null binding.urlView.error = null
@ -164,41 +160,34 @@ class LoginActivity :
repository.refreshLoginInformation(url, login, password) repository.refreshLoginInformation(url, login, password)
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
repository.updateApiInformation() repository.updateApiInformation()
val result = repository.login()
CountingIdlingResourceSingleton.increment()
launch(Dispatchers.Main) {
if (result) {
val errorFetching = repository.checkIfFetchFails()
if (!errorFetching) {
goToMain()
} else {
preferenceError()
}
} else {
preferenceError()
}
CountingIdlingResourceSingleton.decrement()
}
} catch (e: Exception) { } catch (e: Exception) {
CountingIdlingResourceSingleton.increment() if (e.message?.startsWith("No transformation found") == true) {
launch(Dispatchers.Main) { Toast
if (e.message?.startsWith("No transformation found") == true) { .makeText(
Toast applicationContext,
.makeText( R.string.application_selfoss_only,
applicationContext, Toast.LENGTH_LONG,
R.string.application_selfoss_only, ).show()
Toast.LENGTH_LONG, preferenceError()
).show() showProgress(false)
preferenceError()
}
CountingIdlingResourceSingleton.decrement()
} }
} finally {
CountingIdlingResourceSingleton.decrement()
} }
val result = repository.login()
if (result) {
val errorFetching = repository.checkIfFetchFails()
if (!errorFetching) {
goToMain()
} else {
preferenceError()
}
} else {
preferenceError()
}
showProgress(false)
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -311,7 +300,6 @@ class LoginActivity :
.withAboutSpecial2Description(AppSettingsService.BUG_URL) .withAboutSpecial2Description(AppSettingsService.BUG_URL)
.withAboutSpecial1("Project Page") .withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL) .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.withShowLoadingProgress(false)
.start(this) .start(this)
true true
} }

View File

@ -73,7 +73,7 @@ class MyApp :
), ),
) )
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
connectivityService.networkAvailableProvider.collect { networkAvailable -> connectivityService.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = val toastMessage =
if (networkAvailable) { if (networkAvailable) {
@ -82,14 +82,13 @@ class MyApp :
} else { } else {
R.string.network_connectivity_lost R.string.network_connectivity_lost
} }
launch(Dispatchers.Main) {
Toast Toast
.makeText( .makeText(
applicationContext, applicationContext,
toastMessage, toastMessage,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
).show() ).show()
}
} }
} }
} }

View File

@ -27,7 +27,7 @@ class ReaderActivity :
DIAware { DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private var toolbarMenu: Menu? = null private lateinit var toolbarMenu: Menu
private lateinit var binding: ActivityReaderBinding private lateinit var binding: ActivityReaderBinding
@ -90,10 +90,8 @@ class ReaderActivity :
} }
private fun updateStarIcon() { private fun updateStarIcon() {
if (toolbarMenu != null) { val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
val isStarred = allItems.getOrNull(currentItem)?.starred ?: false toolbarMenu.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
toolbarMenu!!.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
}
} }
override fun onSaveInstanceState(oldInstanceState: Bundle) { override fun onSaveInstanceState(oldInstanceState: Bundle) {
@ -135,10 +133,8 @@ class ReaderActivity :
private fun alignmentMenu() { private fun alignmentMenu() {
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
if (toolbarMenu != null) { toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
toolbarMenu!!.findItem(R.id.align_left).isVisible = !showJustify toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
toolbarMenu!!.findItem(R.id.align_justify).isVisible = showJustify
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -50,7 +50,6 @@ class SourcesActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
CountingIdlingResourceSingleton.increment()
val mLayoutManager = LinearLayoutManager(this) val mLayoutManager = LinearLayoutManager(this)
var items: ArrayList<SelfossModel.SourceDetail> var items: ArrayList<SelfossModel.SourceDetail>
@ -58,28 +57,25 @@ class SourcesActivity :
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
CoroutineScope(Dispatchers.IO).launch { CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSourcesDetails() val response = repository.getSourcesDetails()
CountingIdlingResourceSingleton.increment() if (response.isNotEmpty()) {
launch(Dispatchers.Main) { items = response
if (response.isNotEmpty()) { val mAdapter =
items = response SourcesListAdapter(
val mAdapter = this@SourcesActivity,
SourcesListAdapter( items,
this@SourcesActivity, )
items, binding.recyclerView.adapter = mAdapter
) mAdapter.notifyDataSetChanged()
binding.recyclerView.adapter = mAdapter } else {
mAdapter.notifyDataSetChanged() Toast
} else { .makeText(
Toast this@SourcesActivity,
.makeText( R.string.cant_get_sources,
this@SourcesActivity, Toast.LENGTH_SHORT,
R.string.cant_get_sources, ).show()
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }

View File

@ -9,7 +9,6 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@ -109,42 +108,36 @@ class UpsertSourceActivity :
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
try { try {
val items = repository.getSpouts() val items = repository.getSpouts()
CountingIdlingResourceSingleton.increment() if (items.isNotEmpty()) {
launch(Dispatchers.Main) { val itemsStrings = items.map { it.value.name }
if (items.isNotEmpty()) { for ((key, value) in items) {
val itemsStrings = items.map { it.value.name } spoutsKV[value.name] = key
for ((key, value) in items) {
spoutsKV[value.name] = key
}
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@UpsertSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings,
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter
if (existingSource != null) {
initFields(items)
}
} else {
handleSpoutFailure()
} }
CountingIdlingResourceSingleton.decrement()
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@UpsertSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings,
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter
if (existingSource != null) {
initFields(items)
}
} else {
handleSpoutFailure()
} }
} catch (e: NetworkUnavailableException) { } catch (e: NetworkUnavailableException) {
handleSpoutFailure(networkIssue = true) handleSpoutFailure(networkIssue = true)
} }
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -167,8 +160,7 @@ class UpsertSourceActivity :
} }
else -> { else -> {
CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
val successfullyAddedSource = val successfullyAddedSource =
if (existingSource != null) { if (existingSource != null) {
repository.updateSource( repository.updateSource(
@ -186,21 +178,16 @@ class UpsertSourceActivity :
binding.tags.text.toString(), binding.tags.text.toString(),
) )
} }
CountingIdlingResourceSingleton.increment() if (successfullyAddedSource) {
launch(Dispatchers.Main) { finish()
if (successfullyAddedSource) { } else {
finish() Toast
} else { .makeText(
Toast this@UpsertSourceActivity,
.makeText( R.string.cant_create_source,
this@UpsertSourceActivity, Toast.LENGTH_SHORT,
R.string.cant_create_source, ).show()
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
} }
CountingIdlingResourceSingleton.decrement()
} }
} }
} }

View File

@ -30,7 +30,7 @@ import org.kodein.di.instance
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit, override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
override lateinit var binding: CardItemBinding override lateinit var binding: CardItemBinding

View File

@ -21,7 +21,7 @@ import org.kodein.di.instance
class ItemListAdapter( class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit, override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
override lateinit var binding: ListItemBinding override lateinit var binding: ListItemBinding

View File

@ -21,7 +21,7 @@ import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
RecyclerView.Adapter<VH>(), RecyclerView.Adapter<VH>(),
DIAware { DIAware {
abstract var items: ArrayList<SelfossModel.Item> abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository abstract val repository: Repository
abstract val binding: ViewBinding abstract val binding: ViewBinding
abstract val appSettingsService: AppSettingsService abstract val appSettingsService: AppSettingsService
@ -31,7 +31,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
protected val c: Context get() = app.baseContext protected val c: Context get() = app.baseContext
fun updateAllItems(items: ArrayList<SelfossModel.Item>) { fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
this.items = items this.items.clear()
this.items.addAll(items)
updateHomeItems(items) updateHomeItems(items)
notifyDataSetChanged() notifyDataSetChanged()
} }

View File

@ -12,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@ -105,10 +104,8 @@ class SourcesListAdapter(
source: SelfossModel.SourceDetail, source: SelfossModel.SourceDetail,
position: Int, position: Int,
) { ) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(source.id, source.title) val successfullyDeletedSource = repository.deleteSource(source.id, source.title)
CountingIdlingResourceSingleton.increment()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (successfullyDeletedSource) { if (successfullyDeletedSource) {
items.removeAt(position) items.removeAt(position)
@ -122,9 +119,7 @@ class SourcesListAdapter(
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
).show() ).show()
} }
CountingIdlingResourceSingleton.decrement()
} }
CountingIdlingResourceSingleton.decrement()
} }
} }
} }

View File

@ -14,7 +14,6 @@ import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
@ -60,14 +59,12 @@ class FilterSheetFragment :
) )
try { try {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
handleTagChips() handleTagChips()
handleSourceChips() handleSourceChips()
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
CountingIdlingResourceSingleton.decrement()
} }
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
dismiss() dismiss()

View File

@ -124,7 +124,6 @@ class SettingsActivity :
LibsBuilder() LibsBuilder()
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withShowLoadingProgress(false)
.start(it) .start(it)
} }
true true

View File

@ -1,17 +0,0 @@
**v125030901**
- Merge pull request 'fix-reload' (#195) from fix-reload into master
- fix: Infinite scroll needs loading stats.
- fix: do not reload items on resume.
- Merge pull request 'tests' (#193) from tests into master
- ci: Instrumentation tests coverage in ci.
- ci: Instrumentation tests coverage in ci.
- ci: Instrumentation tests coverage in ci.
- chore: better handling of coroutine dispatchers.
- ci: Instrumentation tests coverage in ci.
- chore: comment robolectric tests for now.
- fix: Fixed source deletion test.
- Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
- Refactor star icon handling
- Don't restart activity changing alignment
- Changelog for v125030711

View File

@ -1,4 +0,0 @@
**v125040991**
- fix: Connectivity toast message was causing issues.
- Changelog for v125030901

View File

@ -27,4 +27,3 @@ org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none kotlin.native.cacheKind.iosX64=none
org.gradle.configureondemand=true org.gradle.configureondemand=true
kotlin.jvm.target.validation.mode=IGNORE

View File

@ -224,25 +224,16 @@ 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()) {
sources = sourceDetails(isDatabaseEnabled) val apiSources = api.sourcesDetailed()
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
} }
@ -334,7 +325,7 @@ class Repository(
_badgeUnread.value -= 1 _badgeUnread.value -= 1
} }
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item) updateDBItem(item)
} }
} }
@ -345,7 +336,7 @@ class Repository(
_badgeUnread.value += 1 _badgeUnread.value += 1
} }
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item) updateDBItem(item)
} }
} }
@ -356,7 +347,7 @@ class Repository(
_badgeStarred.value += 1 _badgeStarred.value += 1
} }
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item) updateDBItem(item)
} }
} }
@ -367,7 +358,7 @@ class Repository(
_badgeStarred.value -= 1 _badgeStarred.value -= 1
} }
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
updateDBItem(item) updateDBItem(item)
} }
} }
@ -380,7 +371,6 @@ 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,
@ -402,7 +392,6 @@ 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
} }
@ -417,7 +406,6 @@ 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

View File

@ -32,7 +32,6 @@ import io.ktor.utils.io.charsets.Charsets
import io.ktor.utils.io.core.toByteArray import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -83,7 +82,7 @@ class SelfossApi(
} }
modifyRequest { modifyRequest {
Napier.i("Will modify", tag = "HttpSend") Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend") Napier.i("Will login", tag = "HttpSend")
login() login()
Napier.i("Did login", tag = "HttpSend") Napier.i("Did login", tag = "HttpSend")

View File

@ -16,7 +16,7 @@ class ConnectivityService {
fun start() { fun start() {
connectivity = Connectivity() connectivity = Connectivity()
connectivity.start() connectivity.start()
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
connectivity.statusUpdates.collect { status -> connectivity.statusUpdates.collect { status ->
when (status) { when (status) {
is Connectivity.Status.Connected -> { is Connectivity.Status.Connected -> {