Compare commits

...

7 Commits

Author SHA1 Message Date
e826318e12 ci: Instrumentation tests coverage in ci.
Some checks failed
Check PR code / EspressoReports (pull_request) Has been cancelled
2025-03-19 22:58:10 +01:00
02d503e03a chore: comment robolectric tests for now. 2025-03-16 15:44:02 +01:00
24b9320d6d fix: Fixed source deletion test. 2025-03-16 14:27:30 +01:00
ceba58e98f Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
All checks were successful
Check master code / build (push) Successful in 8m27s
Reviewed-on: #190
2025-03-16 13:14:44 +00:00
c3ee07dd85 Refactor star icon handling
All checks were successful
Check PR code / translations (pull_request) Successful in 1m4s
Check PR code / Lint (pull_request) Successful in 1m25s
Check PR code / build (pull_request) Successful in 11m52s
Extracted all favorite handling to two functions. Makes it a little bit more readable.
2025-03-12 16:07:49 +01:00
93d99192b3 Don't restart activity changing alignment
When changing alignment in the reader we were restarting the reader activity to reload. Doing this led to reloading the article which was initially opened every time you changed alignment. Now, when changing the alignment we retain all existing fragments but command all of them to update their alignment setting.
2025-03-12 16:07:49 +01:00
359dec2ca0 Changelog for v125030711 2025-03-12 11:39:57 +00:00
18 changed files with 444 additions and 191 deletions

View File

@ -0,0 +1,55 @@
name: Coverage
on:
workflow_call:
jobs:
BuildAndTestAndCoverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
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
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
arch: x86_64
script: ./gradlew androidApp:connectedAndroidTest
pre-emulator-launch-script: sdkmanager --list | grep system-images
- uses: actions/upload-artifact@v3
if: failure()
with:
name: failure-espresso
path: build/reports/androidTests/connected/screenshots
retention-days: 2
overwrite: true
include-hidden-files: true
- uses: actions/upload-artifact@v3
with:
name: coverage-espresso
path: build/reports/coverage/androidTest/githubConfig/debug/connected
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -3,89 +3,91 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
- chore-crowdin-ci
jobs: jobs:
Lint: 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
cache: gradle # with:
- name: Install klint # distribution: 'temurin'
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ # java-version: '17'
- name: Install detekt # cache: gradle
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 # - name: Install klint
- name: Linting... # run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' # - name: Install detekt
- name: Detecting... # 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
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' # - name: Linting...
translations: # run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
runs-on: ubuntu-latest # - name: Detecting...
steps: # run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
- name: Check out repository code # translations:
uses: actions/checkout@v4 # runs-on: ubuntu-latest
with: # steps:
fetch-depth: 0 # - name: Check out repository code
- name: "Check translations changes" # uses: actions/checkout@v4
id: check-translations-changes # with:
uses: tj-actions/changed-files@v45 # fetch-depth: 0
with: # - 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-api-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-api-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-api-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-api-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-api-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-api-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/common_build.yml # branch: ${{ github.head_ref || github.ref_name }}
# build:
# needs: Lint
# uses: ./.gitea/workflows/common_build.yml

View File

@ -1,3 +1,14 @@
**v125030711
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681
--------------------------------------------------------------------
**v125030681 **v125030681
- chore: do not send reports on simulators. - chore: do not send reports on simulators.

View File

@ -96,6 +96,7 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true" testInstrumentationRunnerArguments["clearPackageData"] = "true"
testInstrumentationRunnerArguments["useTestStorageService"] = "true"
} }
packaging { packaging {
resources { resources {
@ -109,6 +110,11 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
} }
getByName("debug") { getByName("debug") {
isTestCoverageEnabled = true
enableAndroidTestCoverage = true
installation {
installOptions("-g", "-r")
}
} }
} }
flavorDimensions.add("build") flavorDimensions.add("build")
@ -197,14 +203,16 @@ dependencies {
testImplementation("io.mockk:mockk:1.13.14") testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
androidTestImplementation("androidx.test:runner:1.6.2") androidTestImplementation("androidx.test:runner:1.7.0-alpha01")
androidTestImplementation("androidx.test:rules:1.6.1") androidTestImplementation("androidx.test:rules:1.7.0-alpha01")
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.5.1") androidTestUtil("androidx.test:orchestrator: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.6.1") testImplementation("androidx.test:core-ktx:1.7.0-alpha01")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion")
@ -227,6 +235,7 @@ tasks.withType<Test> {
} }
aboutLibraries { aboutLibraries {
excludeFields = arrayOf("generated")
offlineMode = true offlineMode = true
fetchRemoteLicense = false fetchRemoteLicense = false
fetchRemoteFunding = false fetchRemoteFunding = false
@ -235,3 +244,40 @@ aboutLibraries {
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
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 =
tasks.register<Exec>("clearScreenshots") {
println("AMINE : clear")
commandLine = listOf("adb", "shell", "rm", "-r", "/sdcard/Pictures/selfoss_tests")
}
val createScreenshotDirectoryTask =
tasks.register<Exec>("createScreenshotDirectory") {
println("AMINE : create directory")
group = "reporting"
commandLine = listOf("adb", "shell", "mkdir", "-p", "/sdcard/Pictures/selfoss_tests")
}
val fetchScreenshotsTask =
tasks.register<Exec>("fetchScreenshots") {
println("AMINE : fetch")
group = "reporting"
executable(android.adbExecutable.toString())
commandLine = listOf("adb", "pull", "/sdcard/Pictures/selfoss_tests/.", reportsDirectory.toString())
finalizedBy(clearScreenshotsTask)
dependsOn(createScreenshotDirectoryTask)
doFirst {
reportsDirectory.mkdirs()
}
}
tasks.whenTaskAdded {
if (this.name == "connectedAndroidTest") {
this.finalizedBy(fetchScreenshotsTask)
}
}

View File

@ -1,7 +1,11 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import android.os.Environment.DIRECTORY_PICTURES
import android.os.Environment.getExternalStoragePublicDirectory
import android.util.Log
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
@ -9,13 +13,25 @@ import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView 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.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.runner.screenshot.BasicScreenCaptureProcessor
import androidx.test.runner.screenshot.Screenshot
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.hasToString import org.hamcrest.Matchers.hasToString
import org.junit.BeforeClass
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.io.File
import java.io.IOException
import java.util.Locale
fun performLogin(someUrl: String? = null) { fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform( onView(withId(R.id.urlView)).perform(click()).perform(
@ -119,3 +135,86 @@ fun testAddSourceWithUrl(
.perform(click()) .perform(click())
onView(withText(sourceName)).check(matches(isDisplayed())) onView(withText(sourceName)).check(matches(isDisplayed()))
} }
open class WithANRException {
companion object {
// Running count of the number of Android Not Responding dialogues to prevent endless dismissal.
private var anrCount = 0
// `RootViewWithoutFocusException` class is private, need to match the message (instead of using type matching).
private val rootViewWithoutFocusExceptionMsg =
java.lang.String.format(
Locale.ROOT,
"Waited for the root of the view hierarchy to have " +
"window focus and not request layout for 10 seconds. If you specified a non " +
"default root matcher, it may be picking a root that never takes focus. " +
"Root:",
)
private fun handleAnrDialogue() {
val device = UiDevice.getInstance(getInstrumentation())
// If running the device in English Locale
val waitButton = device.findObject(UiSelector().textContains("wait"))
if (waitButton.exists()) waitButton.click()
}
@JvmStatic
@BeforeClass
fun setUpHandler() {
Espresso.setFailureHandler { error, viewMatcher ->
if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) && anrCount < 3) {
anrCount++
handleAnrDialogue()
} else { // chain all failures down to the default espresso handler
DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
}
}
}
}
}
class MyScreenCaptureProcessor(
parentFolderPath: String,
) : BasicScreenCaptureProcessor() {
init {
this.mDefaultScreenshotPath =
File(
File(
getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
"selfoss_tests",
).absolutePath,
"screenshots/$parentFolderPath",
)
}
override fun getFilename(prefix: String): String = prefix
}
fun takeScreenshot(
parentFolderPath: String = "",
screenShotName: String,
) {
Log.d("Screenshots", "Taking screenshot of '$screenShotName'")
val screenCapture = Screenshot.capture()
val processors = setOf(MyScreenCaptureProcessor(parentFolderPath))
try {
screenCapture.apply {
name = screenShotName
process(processors)
}
Log.d("Screenshots", "Screenshot taken")
} catch (ex: IOException) {
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

@ -18,14 +18,22 @@ 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
class HomeActivityTest { class HomeActivityTest : 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 init() { fun init() {
loginAndInitHome() loginAndInitHome()
@ -33,7 +41,7 @@ class HomeActivityTest {
@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

@ -17,14 +17,22 @@ 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
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class LoginActivityTest { 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

View File

@ -24,14 +24,22 @@ 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
class SettingsActivityGeneralTest { class SettingsActivityGeneralTest : 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 init() { fun init() {
loginAndInitHome() loginAndInitHome()

View File

@ -19,14 +19,22 @@ 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
class SettingsActivityOfflineTest { class SettingsActivityOfflineTest : 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())
lateinit var context: Context lateinit var context: Context
@Before @Before

View File

@ -17,14 +17,22 @@ 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
class SettingsActivityReaderTest { class SettingsActivityReaderTest : 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())
lateinit var context: Context lateinit var context: Context
@Before @Before

View File

@ -15,13 +15,22 @@ 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
class SettingsActivityTest { class SettingsActivityTest : 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())
lateinit var context: Context lateinit var context: Context
@Before @Before

View File

@ -18,15 +18,23 @@ 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
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SourcesActivityTest { class SourcesActivityTest : 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())
lateinit var sourceName: String lateinit var sourceName: String
@Before @Before
@ -71,6 +79,8 @@ class SourcesActivityTest {
fun deleteTheCreatedSource() { fun deleteTheCreatedSource() {
onView(withText(sourceName)).check(matches(isDisplayed())) onView(withText(sourceName)).check(matches(isDisplayed()))
onView(withId(R.id.deleteBtn)).perform(click()) onView(withId(R.id.deleteBtn)).perform(click())
onView(withText(R.string.confirm_delete_title)).check(matches(isDisplayed()))
onView(withId(android.R.id.button1)).perform(click())
onView(withText(sourceName)).check(doesNotExist()) onView(withText(sourceName)).check(doesNotExist())
} }

View File

@ -37,22 +37,6 @@ class ReaderActivity :
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
} else {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
}
}
private fun canFavorite() {
showMenuItem(true)
}
private fun canRemoveFromFavorite() {
showMenuItem(false)
}
@Suppress("detekt:SwallowedException") @Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -73,14 +57,21 @@ class ReaderActivity :
finish() finish()
} }
try { readItem()
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
finish()
}
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false) binding.pager.setCurrentItem(currentItem, false)
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentItem = position
updateStarIcon()
readItem()
}
},
)
} }
override fun onResume() { override fun onResume() {
@ -89,14 +80,20 @@ class ReaderActivity :
binding.indicator.setViewPager(binding.pager) binding.indicator.setViewPager(binding.pager)
} }
private fun readItem(item: SelfossModel.Item) { private fun readItem() {
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) { val item = allItems.getOrNull(currentItem)
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess() && item != null) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item) repository.markAsRead(item)
} }
} }
} }
private fun updateStarIcon() {
val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
toolbarMenu.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
}
override fun onSaveInstanceState(oldInstanceState: Bundle) { override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState) super.onSaveInstanceState(oldInstanceState)
oldInstanceState.clear() oldInstanceState.clear()
@ -141,8 +138,7 @@ class ReaderActivity :
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater menuInflater.inflate(R.menu.reader_menu, menu)
inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu toolbarMenu = menu
alignmentMenu() alignmentMenu()
@ -150,87 +146,50 @@ class ReaderActivity :
if (appSettingsService.getPublicAccess()) { if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star) menu.removeItem(R.id.star)
} else { } else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) { updateStarIcon()
canRemoveFromFavorite()
} else {
canFavorite()
}
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (!allItems.isNullOrEmpty() && allItems.size >= position) {
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
}
},
)
} }
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
canFavorite()
}
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> onBackPressedDispatcher.onBackPressed()
onBackPressedDispatcher.onBackPressed() R.id.star -> toggleFavorite()
return true R.id.align_left -> switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
} R.id.align_justify -> switchAlignmentSetting(AppSettingsService.JUSTIFY)
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem])
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem])
}
afterSave()
}
}
R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment()
}
R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment()
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun switchAlignmentSetting(allignment: Int) { private fun toggleFavorite() {
appSettingsService.changeAllignment(allignment) val item = allItems.getOrNull(currentItem) ?: return
alignmentMenu()
val starred = item.starred
CoroutineScope(Dispatchers.IO).launch {
if (starred) {
repository.unstarr(item)
} else {
repository.starr(item)
}
}
item.toggleStar()
updateStarIcon()
} }
private fun refreshFragment() { private fun switchAlignmentSetting(alignment: Int) {
finish() appSettingsService.changeAllignment(alignment)
overridePendingTransition(0, 0) alignmentMenu()
startActivity(intent)
overridePendingTransition(0, 0) val fragmentManager = supportFragmentManager
val fragments = fragmentManager.fragments
for (fragment in fragments) {
if (fragment is ArticleFragment) {
fragment.refreshAlignment()
}
}
} }
} }

View File

@ -263,13 +263,15 @@ class ArticleFragment :
) )
} }
private fun refreshAlignment() { fun refreshAlignment() {
textAlignment = textAlignment =
when (appSettingsService.getActiveAllignment()) { when (appSettingsService.getActiveAllignment()) {
1 -> "justify" 1 -> "justify"
2 -> "left" 2 -> "left"
else -> "justify" else -> "justify"
} }
htmlToWebview()
} }
@Suppress("detekt:SwallowedException") @Suppress("detekt:SwallowedException")

View File

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.view.Menu import android.view.Menu
@ -25,3 +27,4 @@ fun Menu.assertVisible(
val item = this.findItem(id) val item = this.findItem(id)
assertTrue(item.isVisible) assertTrue(item.isVisible)
} }
*/

View File

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.widget.Button import android.widget.Button
@ -57,7 +59,8 @@ class LoginActivityTest {
} }
} }
/* @Test */
/* @Test
fun connect() { fun connect() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller -> Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state controller.setup() // Moves the Activity to the RESUMED state
@ -72,4 +75,7 @@ class LoginActivityTest {
assertEquals(expectedIntent.component, actual.component) assertEquals(expectedIntent.component, actual.component)
} }
}*/ }*/
/*
} }
*/

View File

@ -1,3 +1,5 @@
@file:Suppress("ktlint")
/*
package bou.amine.apps.readerforselfossv2.android.tests.robolectric package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
@ -8,3 +10,4 @@ class RobotElectriqueRunner(
) : RobolectricTestRunner(testClass) { ) : RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build() override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
} }
*/

View File

@ -0,0 +1,8 @@
**v125030711**
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681