From 2c88a4efa855a02183c2f0f4a06194adbee3f4ad Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Mon, 5 May 2025 11:05:18 +0100 Subject: [PATCH 01/13] configure all tests to use junit4 --- app/build.gradle.kts | 31 ++++++++++++------ .../be/scri/ui/screens/about/AboutUtilTest.kt | 7 ++++ .../helpers/AlphanumericComparatorTest.kt | 32 +++++++++---------- 3 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0a9db16d7..528cfce11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("org.jmailen.kotlinter") id("io.gitlab.arturbosch.detekt") id("com.google.devtools.ksp") version "2.0.0-1.0.22" apply true - id("de.mannodermaus.android-junit5") version "1.11.2.0" +// id("de.mannodermaus.android-junit5") version "1.11.2.0" id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" id("jacoco") kotlin("plugin.serialization") version "1.9.0" @@ -28,7 +28,7 @@ jacoco { } val kotlinVersion by extra("2.0.0") -val junit5Version by extra("5.11.2") +//val junit5Version by extra("5.11.2") val mockkVersion by extra("1.13.13") android { @@ -112,6 +112,9 @@ android { sourceSets { getByName("main").java.srcDirs("src/main/kotlin") + named("test") { + java.srcDirs("src/test/java", "src/test/kotlin") + } } lint { @@ -230,12 +233,22 @@ dependencies { implementation("androidx.navigation:navigation-compose:$2.8.4") // Testing libraries - testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") +// testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") testImplementation("io.mockk:mockk:$mockkVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.13") + testImplementation("androidx.compose.ui:ui-test-junit4:1.8.0") // Compose UI Testing for unit tests + testImplementation("org.jetbrains.kotlin:kotlin-test:2.0.20") + testImplementation ("org.mockito:mockito-core:5.12.0") + testImplementation ("org.mockito:mockito-inline:5.2.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") +// testImplementation ("org.mockito:mockito-junit-jupiter:5.12.0") + // For Instrumentation Tests - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.5") + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.0") + androidTestImplementation("io.mockk:mockk-android:$mockkVersion") debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.5") // Espresso for UI tests @@ -246,8 +259,8 @@ dependencies { androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") // JUnit 5 dependencies - testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") +// testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") +// testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") // AndroidJUnit4 is included androidTestImplementation("androidx.test.ext:junit:1.2.1") @@ -306,9 +319,9 @@ tasks.withType(Test::class) { } } -tasks.withType { - useJUnitPlatform() -} +//tasks.withType { +// useJUnitPlatform() +//} tasks.register("jacocoTestReport") { group = "Reporting" diff --git a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt new file mode 100644 index 000000000..dee3329a2 --- /dev/null +++ b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt @@ -0,0 +1,7 @@ +package be.scri.ui.screens.about + + +class AboutUtilTest { + + +} diff --git a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt index 5365abafb..f82c8ba43 100644 --- a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt +++ b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt @@ -5,20 +5,20 @@ */ package helpers - +// import be.scri.helpers.AlphanumericComparator -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class AlphanumericComparatorTest { - private val subject = AlphanumericComparator() - - @Test - fun testCompare() { - assertEquals(-1, subject.compare("IMG_10.png", "IMG_11.png")) - - assertEquals(0, subject.compare("IMG_10.png", "IMG_10.png")) - - assertEquals(1, subject.compare("IMG_11.png", "IMG_10.png")) - } -} +//import org.junit.jupiter.api.Assertions.assertEquals +//import org.junit.jupiter.api.Test +// +//class AlphanumericComparatorTest { +// private val subject = AlphanumericComparator() +// +// @Test +// fun testCompare() { +// assertEquals(-1, subject.compare("IMG_10.png", "IMG_11.png")) +// +// assertEquals(0, subject.compare("IMG_10.png", "IMG_10.png")) +// +// assertEquals(1, subject.compare("IMG_11.png", "IMG_10.png")) +// } +//} From 4f2ea7ac24c364a66e77a2776e6db1aebce03366 Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Mon, 5 May 2025 16:01:55 +0100 Subject: [PATCH 02/13] test(AboutUtil): add unit tests for AboutUtil composables and non-UI functions Added tests for the following AboutUtil functions: - Non-UI functions: - onShareScribeClick - onMailClick - Composable functions: - getCommunityList (covers all items and their click behavior) - getFeedbackAndSupportList (covers callback and intent-triggered items) - getLegalListItems Note: `onRateScribeClick` test is pending. --- app/build.gradle.kts | 6 +- .../be/scri/ui/screens/about/AboutUtilTest.kt | 222 +++++++++++++++++- .../helpers/AlphanumericComparatorTest.kt | 32 +-- 3 files changed, 239 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 528cfce11..f04634a84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,12 +66,12 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } buildFeatures { diff --git a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt index dee3329a2..66134b0bc 100644 --- a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt +++ b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt @@ -1,7 +1,225 @@ -package be.scri.ui.screens.about - +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import be.scri.ui.models.ScribeItem +import be.scri.ui.models.ScribeItemList +import be.scri.ui.screens.about.AboutUtil +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.check +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +@RunWith(AndroidJUnit4::class) class AboutUtilTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testGetLegalListItemsTriggersCallbacks() { + var privacyClicked = false + var licensesClicked = false + + lateinit var list: ScribeItemList + + composeTestRule.setContent { + list = + AboutUtil.getLegalListItems( + onPrivacyPolicyClick = { privacyClicked = true }, + onThirdPartyLicensesClick = { licensesClicked = true }, + ) + } + + // Simulate clicks + val privacyItem = list.items[0] as ScribeItem.ExternalLinkItem + val licenseItem = list.items[1] as ScribeItem.ExternalLinkItem + + privacyItem.onClick() + licenseItem.onClick() + + assertTrue(privacyClicked) + assertTrue(licensesClicked) + } + + @Test + fun testGetCommunityListTriggersCallbacks() { + var shareClicked = false + var wikiClicked = false + + lateinit var list: ScribeItemList + val mockContext = mock() + + composeTestRule.setContent { + list = + AboutUtil.getCommunityList( + onWikimediaAndScribeClick = { wikiClicked = true }, + onShareScribeClick = { shareClicked = true }, + context = mockContext, + ) + } + + // Last two are share and Wikimedia + (list.items[0] as ScribeItem.ExternalLinkItem).onClick() + (list.items[1] as ScribeItem.ExternalLinkItem).onClick() + (list.items[2] as ScribeItem.ExternalLinkItem).onClick() + (list.items[3] as ScribeItem.ExternalLinkItem).onClick() + (list.items[4] as ScribeItem.ExternalLinkItem).onClick() + + assertTrue(shareClicked) + assertTrue(wikiClicked) + + // Verify that the URLs for GitHub, Matrix, and Mastodon are correctly opened + verify(mockContext).startActivity( + check { + assertEquals(Intent.ACTION_VIEW, it.action) + assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android"), it.data) + }, + ) + + verify(mockContext).startActivity( + check { + assertEquals(Intent.ACTION_VIEW, it.action) + assertEquals(Uri.parse("https://matrix.to/%23/%23scribe_community:matrix.org"), it.data) + }, + ) + + verify(mockContext).startActivity( + check { + assertEquals(Intent.ACTION_VIEW, it.action) + assertEquals(Uri.parse("https://wikis.world/@scribe"), it.data) + }, + ) + } + + @Test + fun testGetFeedbackAndSupportListTriggersCallbacks() { + var rateClicked = false + var mailClicked = false + var resetClicked = false + + lateinit var list: ScribeItemList + val mockContext = mock() + val mockPackageManager = mock() + + // Stub context methods + whenever(mockContext.packageManager).thenReturn(mockPackageManager) + + composeTestRule.setContent { + list = + AboutUtil.getFeedbackAndSupportList( + onRateScribeClick = { rateClicked = true }, + onMailClick = { mailClicked = true }, + onResetHintsClick = { resetClicked = true }, + context = mockContext, + ) + } + + // Check correct item clicks + (list.items[0] as ScribeItem.ExternalLinkItem).onClick() // rate + (list.items[1] as ScribeItem.ExternalLinkItem).onClick() // report Bug + (list.items[2] as ScribeItem.ExternalLinkItem).onClick() // mail + (list.items[3] as ScribeItem.ExternalLinkItem).onClick() // version + (list.items[4] as ScribeItem.ExternalLinkItem).onClick() // reset + + assertTrue(rateClicked) + assertTrue(mailClicked) + assertTrue(resetClicked) + + // Verify Report Bug opens the correct URL + verify(mockContext).startActivity( + check { + assertEquals(Intent.ACTION_VIEW, it.action) + assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android/issues"), it.data) + }, + ) + + // Verify Version opens the correct URL + verify(mockContext).startActivity( + check { + assertEquals(Intent.ACTION_VIEW, it.action) + assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android/releases/"), it.data) + }, + ) + } + + @Test + fun testOnShareScribeClick() { + val mockContext = mock(Context::class.java) + + // Call the function under test + AboutUtil.onShareScribeClick(mockContext) + + // Capture arguments + val intentCaptor = argumentCaptor() + val bundleCaptor = argumentCaptor() + + verify(mockContext).startActivity(intentCaptor.capture(), bundleCaptor.capture()) + + val chooserIntent = intentCaptor.firstValue + + // Check that it's a chooser + assertEquals(Intent.ACTION_CHOOSER, chooserIntent.action) + + // Extract the wrapped share intent + val actualShareIntent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT) + + // Validate share intent + assertEquals(Intent.ACTION_SEND, actualShareIntent?.action) + assertEquals("text/plain", actualShareIntent?.type) + assertEquals( + "https://github.com/scribe-org/Scribe-Android", + actualShareIntent?.getStringExtra(Intent.EXTRA_TEXT), + ) + + // Optional: Validate chooser title + assertEquals( + "Share via", + chooserIntent.getStringExtra(Intent.EXTRA_TITLE), + ) // Replace with actual title if needed + + // Bundle (2nd arg to startActivity) is expected to be null + assertNull(bundleCaptor.firstValue) + } + + @Test + fun testOnMailClick_sendsEmailIntent() { + val mockContext = mock() + + // Stub startActivity to avoid crash + doNothing().whenever(mockContext).startActivity(any(), anyOrNull()) + + // Call the function + AboutUtil.onMailClick(mockContext) + // Capture intent passed to startActivity + argumentCaptor().apply { + verify(mockContext).startActivity(capture(), eq(null)) + val chooserIntent = firstValue + // Extract the actual email intent from the chooser + val actualEmailIntent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT) + assertNotNull(actualEmailIntent) + assertEquals(Intent.ACTION_SEND, actualEmailIntent?.action) + assertEquals("message/rfc822", actualEmailIntent?.type) + assertArrayEquals(arrayOf("team@scri.be"), actualEmailIntent?.getStringArrayExtra(Intent.EXTRA_EMAIL)) + assertEquals("Hey Scribe!", actualEmailIntent?.getStringExtra(Intent.EXTRA_SUBJECT)) + } + } } diff --git a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt index f82c8ba43..f93e2d2d6 100644 --- a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt +++ b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt @@ -5,20 +5,20 @@ */ package helpers -// + import be.scri.helpers.AlphanumericComparator -//import org.junit.jupiter.api.Assertions.assertEquals -//import org.junit.jupiter.api.Test -// -//class AlphanumericComparatorTest { -// private val subject = AlphanumericComparator() -// -// @Test -// fun testCompare() { -// assertEquals(-1, subject.compare("IMG_10.png", "IMG_11.png")) -// -// assertEquals(0, subject.compare("IMG_10.png", "IMG_10.png")) -// -// assertEquals(1, subject.compare("IMG_11.png", "IMG_10.png")) -// } -//} +import org.junit.Assert.assertEquals +import org.junit.Test + +class AlphanumericComparatorTest { + private val subject = AlphanumericComparator() + + @Test + fun testCompare() { + assertEquals(-1, subject.compare("IMG_10.png", "IMG_11.png")) + + assertEquals(0, subject.compare("IMG_10.png", "IMG_10.png")) + + assertEquals(1, subject.compare("IMG_11.png", "IMG_10.png")) + } +} From 825351498371bc0530e5aa6a01773a8805151ad5 Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Mon, 5 May 2025 16:24:03 +0100 Subject: [PATCH 03/13] add license header --- .../test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt index 66134b0bc..429a254e9 100644 --- a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt +++ b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +@file:Suppress("ktlint:standard:kdoc") +/** + * Testing for AboutUtil. + */ + import android.content.Context import android.content.Intent import android.content.pm.PackageManager From 57fe5d9117dd7dd6faf2cbf6241b40c36cb65f7c Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Mon, 9 Jun 2025 13:39:17 +0100 Subject: [PATCH 04/13] add AboutUtil Instrumentation tests --- app/build.gradle.kts | 14 +- .../about/AboutUtilInstrumentedTest.kt | 270 ++++++++++++++++++ .../be/scri/ui/screens/about/AboutUtilTest.kt | 231 --------------- 3 files changed, 283 insertions(+), 232 deletions(-) create mode 100644 app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt delete mode 100644 app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ff73f63c9..4bebb95cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ val kotlinVersion by extra("2.0.0") val mockkVersion by extra("1.13.13") android { - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "be.scri" @@ -45,6 +45,16 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + packaging { + resources { + pickFirsts.add("META-INF/LICENSE*") + pickFirsts.add("META-INF/ASL2.0") + pickFirsts.add("META-INF/NOTICE*") + pickFirsts.add("META-INF/LGPL2.1") + + } + } + kover { verify { @@ -243,12 +253,14 @@ dependencies { testImplementation ("org.mockito:mockito-core:5.12.0") testImplementation ("org.mockito:mockito-inline:5.2.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") + testImplementation("com.google.truth:truth:1.4.4") // testImplementation ("org.mockito:mockito-junit-jupiter:5.12.0") // For Instrumentation Tests androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.0") androidTestImplementation("io.mockk:mockk-android:$mockkVersion") + androidTestImplementation("com.google.truth:truth:1.4.4") debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.5") // Espresso for UI tests diff --git a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt new file mode 100644 index 000000000..eed93533d --- /dev/null +++ b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package be.scri.ui.screens.about + +import android.content.Intent +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey +import androidx.test.espresso.intent.matcher.IntentMatchers.hasType +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import be.scri.R +import be.scri.activities.MainActivity +import be.scri.ui.models.ScribeItem +import com.google.common.truth.Truth.assertThat +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AboutUtilInstrumentedTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setup() { + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun test_onShareScribeClick_doesNotCrash() { + // Use the app context + val context = InstrumentationRegistry.getInstrumentation().targetContext + + AboutUtil.onShareScribeClick(context) + + Intents.intended(hasAction(Intent.ACTION_CHOOSER)) + } + + @Test + fun test_onRateScribeClick_doesNotCrash() { + val scenario = ActivityScenario.launch(MainActivity::class.java) + scenario.onActivity { activity -> + AboutUtil.onRateScribeClick(activity) + } + } + + @Test + fun test_onMailClick_launchesEmailIntent() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + + AboutUtil.onMailClick(context) + + Intents.intended( + allOf( + hasAction(Intent.ACTION_CHOOSER), + hasExtraWithKey(Intent.EXTRA_INTENT), + hasExtra( + `is`(Intent.EXTRA_INTENT), + allOf( + hasAction(Intent.ACTION_SEND), + hasType("message/rfc822"), + ), + ), + ), + ) + } + + @Test + fun testGetCommunityList() { + println("Testing getCommunityList...") + + var wikimediaClicked = false + var shareClicked = false + val context = InstrumentationRegistry.getInstrumentation().targetContext + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + val communityList = + AboutUtil.getCommunityList( + onWikimediaAndScribeClick = { wikimediaClicked = true }, + onShareScribeClick = { shareClicked = true }, + context = context, + ) + + // Test list is not empty + assertThat(communityList.items).isNotEmpty() + assertThat(communityList.items).hasSize(5) + + // Test each item has required fields + communityList.items.forEach { item -> + assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) + val linkItem = item as ScribeItem.ExternalLinkItem + + assertThat(linkItem.leadingIcon).isNotNull() + assertThat(linkItem.title).isNotNull() + assertThat(linkItem.trailingIcon).isNotNull() + assertThat(linkItem.onClick).isNotNull() + } + + // Test specific items + val githubItem = communityList.items[0] as ScribeItem.ExternalLinkItem + assertThat(githubItem.leadingIcon).isEqualTo(R.drawable.github_logo) + assertThat(githubItem.title).isEqualTo(R.string.app_about_community_github) + + val shareItem = communityList.items[3] as ScribeItem.ExternalLinkItem + assertThat(shareItem.leadingIcon).isEqualTo(R.drawable.share_icon) + assertThat(shareItem.title).isEqualTo(R.string.app_about_community_share_scribe) + + val wikimediaItem = communityList.items[4] as ScribeItem.ExternalLinkItem + assertThat(wikimediaItem.leadingIcon).isEqualTo(R.drawable.wikimedia_logo_black) + assertThat(wikimediaItem.title).isEqualTo(R.string.app_about_community_wikimedia) + + // Test onClick callbacks + shareItem.onClick() + wikimediaItem.onClick() + } + } + + // Verify callbacks were triggered + assertThat(shareClicked).isTrue() + assertThat(wikimediaClicked).isTrue() + + println("getCommunityList test passed!") + } + + @Test + fun testGetFeedbackAndSupportList() { + println("Testing getFeedbackAndSupportList...") + + var rateClicked = false + var mailClicked = false + var resetHintsClicked = false + val context = InstrumentationRegistry.getInstrumentation().targetContext + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + val feedbackList = + AboutUtil.getFeedbackAndSupportList( + onRateScribeClick = { rateClicked = true }, + onMailClick = { mailClicked = true }, + onResetHintsClick = { resetHintsClicked = true }, + context = context, + ) + + // Test list is not empty + assertThat(feedbackList.items).isNotEmpty() + assertThat(feedbackList.items).hasSize(5) + + // Test each item has required fields + feedbackList.items.forEach { item -> + assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) + val linkItem = item as ScribeItem.ExternalLinkItem + + assertThat(linkItem.leadingIcon).isNotNull() + assertThat(linkItem.title).isNotNull() + assertThat(linkItem.trailingIcon).isNotNull() + assertThat(linkItem.onClick).isNotNull() + } + + // Test specific items + val rateItem = feedbackList.items[0] as ScribeItem.ExternalLinkItem + assertThat(rateItem.leadingIcon).isEqualTo(R.drawable.star) + assertThat(rateItem.title).isEqualTo(R.string.app_about_feedback_rate_scribe) + + val mailItem = feedbackList.items[2] as ScribeItem.ExternalLinkItem + assertThat(mailItem.leadingIcon).isEqualTo(R.drawable.mail_icon) + assertThat(mailItem.title).isEqualTo(R.string.app_about_feedback_email) + + val hintsItem = feedbackList.items[4] as ScribeItem.ExternalLinkItem + assertThat(hintsItem.leadingIcon).isEqualTo(R.drawable.light_bulb_icon) + assertThat(hintsItem.title).isEqualTo(R.string.app_about_feedback_app_hints) + + // Test onClick callbacks + rateItem.onClick() + mailItem.onClick() + hintsItem.onClick() + } + } + + // Verify callbacks were triggered + assertThat(rateClicked).isTrue() + assertThat(mailClicked).isTrue() + assertThat(resetHintsClicked).isTrue() + + println("getFeedbackAndSupportList test passed!") + } + + @Test + fun testGetLegalListItems() { + println("Testing getLegalListItems...") + + var privacyPolicyClicked = false + var thirdPartyLicensesClicked = false + val context = InstrumentationRegistry.getInstrumentation().targetContext + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + val legalList = + AboutUtil.getLegalListItems( + onPrivacyPolicyClick = { privacyPolicyClicked = true }, + onThirdPartyLicensesClick = { thirdPartyLicensesClicked = true }, + ) + + // Test list is not empty + assertThat(legalList.items).isNotEmpty() + assertThat(legalList.items).hasSize(2) + + // Test each item has required fields + legalList.items.forEach { item -> + assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) + val linkItem = item as ScribeItem.ExternalLinkItem + + assertThat(linkItem.leadingIcon).isNotNull() + assertThat(linkItem.title).isNotNull() + assertThat(linkItem.trailingIcon).isNotNull() + assertThat(linkItem.onClick).isNotNull() + } + + // Test specific items + val privacyItem = legalList.items[0] as ScribeItem.ExternalLinkItem + assertThat(privacyItem.leadingIcon).isEqualTo(R.drawable.shield_lock) + assertThat(privacyItem.title).isEqualTo(R.string.app_about_legal_privacy_policy) + assertThat(privacyItem.trailingIcon).isEqualTo(R.drawable.right_arrow) + + val licenseItem = legalList.items[1] as ScribeItem.ExternalLinkItem + assertThat(licenseItem.leadingIcon).isEqualTo(R.drawable.license_icon) + assertThat(licenseItem.title).isEqualTo(R.string.app_about_legal_third_party) + assertThat(licenseItem.trailingIcon).isEqualTo(R.drawable.right_arrow) + + // Test onClick callbacks + privacyItem.onClick() + licenseItem.onClick() + } + } + + // Verify callbacks were triggered + assertThat(privacyPolicyClicked).isTrue() + assertThat(thirdPartyLicensesClicked).isTrue() + + println("getLegalListItems test passed!") + } + + @Test + fun testExternalLinksConstants() { + println("Testing ExternalLinks constants...") + + // Test that external links are properly defined + assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_SCRIBE).isNotEmpty() + assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_ISSUES).isNotEmpty() + assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_RELEASES).isNotEmpty() + assertThat(be.scri.ui.screens.about.ExternalLinks.MATRIX).isNotEmpty() + assertThat(be.scri.ui.screens.about.ExternalLinks.MASTODON).isNotEmpty() + + println("ExternalLinks constants test passed!") + } +} diff --git a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt b/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt deleted file mode 100644 index 429a254e9..000000000 --- a/app/src/test/kotlin/be/scri/ui/screens/about/AboutUtilTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -@file:Suppress("ktlint:standard:kdoc") -/** - * Testing for AboutUtil. - */ - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Bundle -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import be.scri.ui.models.ScribeItem -import be.scri.ui.models.ScribeItemList -import be.scri.ui.screens.about.AboutUtil -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.mock -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.check -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@RunWith(AndroidJUnit4::class) -class AboutUtilTest { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun testGetLegalListItemsTriggersCallbacks() { - var privacyClicked = false - var licensesClicked = false - - lateinit var list: ScribeItemList - - composeTestRule.setContent { - list = - AboutUtil.getLegalListItems( - onPrivacyPolicyClick = { privacyClicked = true }, - onThirdPartyLicensesClick = { licensesClicked = true }, - ) - } - - // Simulate clicks - val privacyItem = list.items[0] as ScribeItem.ExternalLinkItem - val licenseItem = list.items[1] as ScribeItem.ExternalLinkItem - - privacyItem.onClick() - licenseItem.onClick() - - assertTrue(privacyClicked) - assertTrue(licensesClicked) - } - - @Test - fun testGetCommunityListTriggersCallbacks() { - var shareClicked = false - var wikiClicked = false - - lateinit var list: ScribeItemList - val mockContext = mock() - - composeTestRule.setContent { - list = - AboutUtil.getCommunityList( - onWikimediaAndScribeClick = { wikiClicked = true }, - onShareScribeClick = { shareClicked = true }, - context = mockContext, - ) - } - - // Last two are share and Wikimedia - (list.items[0] as ScribeItem.ExternalLinkItem).onClick() - (list.items[1] as ScribeItem.ExternalLinkItem).onClick() - (list.items[2] as ScribeItem.ExternalLinkItem).onClick() - (list.items[3] as ScribeItem.ExternalLinkItem).onClick() - (list.items[4] as ScribeItem.ExternalLinkItem).onClick() - - assertTrue(shareClicked) - assertTrue(wikiClicked) - - // Verify that the URLs for GitHub, Matrix, and Mastodon are correctly opened - verify(mockContext).startActivity( - check { - assertEquals(Intent.ACTION_VIEW, it.action) - assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android"), it.data) - }, - ) - - verify(mockContext).startActivity( - check { - assertEquals(Intent.ACTION_VIEW, it.action) - assertEquals(Uri.parse("https://matrix.to/%23/%23scribe_community:matrix.org"), it.data) - }, - ) - - verify(mockContext).startActivity( - check { - assertEquals(Intent.ACTION_VIEW, it.action) - assertEquals(Uri.parse("https://wikis.world/@scribe"), it.data) - }, - ) - } - - @Test - fun testGetFeedbackAndSupportListTriggersCallbacks() { - var rateClicked = false - var mailClicked = false - var resetClicked = false - - lateinit var list: ScribeItemList - val mockContext = mock() - val mockPackageManager = mock() - - // Stub context methods - whenever(mockContext.packageManager).thenReturn(mockPackageManager) - - composeTestRule.setContent { - list = - AboutUtil.getFeedbackAndSupportList( - onRateScribeClick = { rateClicked = true }, - onMailClick = { mailClicked = true }, - onResetHintsClick = { resetClicked = true }, - context = mockContext, - ) - } - - // Check correct item clicks - (list.items[0] as ScribeItem.ExternalLinkItem).onClick() // rate - (list.items[1] as ScribeItem.ExternalLinkItem).onClick() // report Bug - (list.items[2] as ScribeItem.ExternalLinkItem).onClick() // mail - (list.items[3] as ScribeItem.ExternalLinkItem).onClick() // version - (list.items[4] as ScribeItem.ExternalLinkItem).onClick() // reset - - assertTrue(rateClicked) - assertTrue(mailClicked) - assertTrue(resetClicked) - - // Verify Report Bug opens the correct URL - verify(mockContext).startActivity( - check { - assertEquals(Intent.ACTION_VIEW, it.action) - assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android/issues"), it.data) - }, - ) - - // Verify Version opens the correct URL - verify(mockContext).startActivity( - check { - assertEquals(Intent.ACTION_VIEW, it.action) - assertEquals(Uri.parse("https://github.com/scribe-org/Scribe-Android/releases/"), it.data) - }, - ) - } - - @Test - fun testOnShareScribeClick() { - val mockContext = mock(Context::class.java) - - // Call the function under test - AboutUtil.onShareScribeClick(mockContext) - - // Capture arguments - val intentCaptor = argumentCaptor() - val bundleCaptor = argumentCaptor() - - verify(mockContext).startActivity(intentCaptor.capture(), bundleCaptor.capture()) - - val chooserIntent = intentCaptor.firstValue - - // Check that it's a chooser - assertEquals(Intent.ACTION_CHOOSER, chooserIntent.action) - - // Extract the wrapped share intent - val actualShareIntent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT) - - // Validate share intent - assertEquals(Intent.ACTION_SEND, actualShareIntent?.action) - assertEquals("text/plain", actualShareIntent?.type) - assertEquals( - "https://github.com/scribe-org/Scribe-Android", - actualShareIntent?.getStringExtra(Intent.EXTRA_TEXT), - ) - - // Optional: Validate chooser title - assertEquals( - "Share via", - chooserIntent.getStringExtra(Intent.EXTRA_TITLE), - ) // Replace with actual title if needed - - // Bundle (2nd arg to startActivity) is expected to be null - assertNull(bundleCaptor.firstValue) - } - - @Test - fun testOnMailClick_sendsEmailIntent() { - val mockContext = mock() - - // Stub startActivity to avoid crash - doNothing().whenever(mockContext).startActivity(any(), anyOrNull()) - - // Call the function - AboutUtil.onMailClick(mockContext) - - // Capture intent passed to startActivity - argumentCaptor().apply { - verify(mockContext).startActivity(capture(), eq(null)) - val chooserIntent = firstValue - - // Extract the actual email intent from the chooser - val actualEmailIntent = chooserIntent.getParcelableExtra(Intent.EXTRA_INTENT) - assertNotNull(actualEmailIntent) - assertEquals(Intent.ACTION_SEND, actualEmailIntent?.action) - assertEquals("message/rfc822", actualEmailIntent?.type) - assertArrayEquals(arrayOf("team@scri.be"), actualEmailIntent?.getStringArrayExtra(Intent.EXTRA_EMAIL)) - assertEquals("Hey Scribe!", actualEmailIntent?.getStringExtra(Intent.EXTRA_SUBJECT)) - } - } -} From 3aa2ad29254a0679c8abd8be0814b052aae0d34d Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 17:06:23 +0100 Subject: [PATCH 05/13] fix gradle file --- app/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4705e846c..54094cc3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,8 @@ jacoco { toolVersion = "0.8.12" } -//val junit5Version by extra("5.11.2") +val kotlinVersion by extra("2.0.0") +val junit5Version by extra("5.11.2") val mockkVersion by extra("1.13.13") android { From 4d6a7200343df6a4d9f4baaa37a4dad37dccbfcc Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 17:26:05 +0100 Subject: [PATCH 06/13] update android CI/CD --- .github/workflows/android_ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 74805c066..45bfbd91a 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -82,7 +82,7 @@ jobs: instrumentation_test: runs-on: ubuntu-latest name: Run Android Instrumentation Tests - timeout-minutes: 30 + timeout-minutes: 50 steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -116,5 +116,7 @@ jobs: adb shell settings put global window_animation_scale 0.0 adb shell settings put global transition_animation_scale 0.0 adb shell settings put global animator_duration_scale 0.0 - sleep 5 + adb shell pm list packages >/dev/null + sleep 10 + adb shell getprop sys.boot_completed ./gradlew connectedCoreDebugAndroidTest --stacktrace From 08b856a8e9cc36357a4e69120caed7dd3ad90d25 Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 17:47:51 +0100 Subject: [PATCH 07/13] fix sharehelper class --- .../java/be/scri/helpers/ui/ShareHelper.kt | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt b/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt index d1d60471a..fe7537512 100644 --- a/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt +++ b/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later package be.scri.helpers.ui +import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.util.Log -import androidx.core.content.ContextCompat /** * A helper to facilitate sharing of the application and contacting the team. @@ -21,20 +21,24 @@ object ShareHelper { */ fun shareScribe(context: Context) { try { - val sharingIntent = - Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "https://github.com/scribe-org/Scribe-Android") + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "https://github.com/scribe-org/Scribe-Android") + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - ContextCompat.startActivity( - context, - Intent.createChooser(sharingIntent, "Share via"), - null, - ) + } + + val chooser = Intent.createChooser(intent, "Share via") + if (context !is Activity) { + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + context.startActivity(chooser) } catch (e: ActivityNotFoundException) { - Log.e("AboutFragment", "No application found to share content", e) + Log.e("ShareHelper", "No application found to share content", e) } catch (e: IllegalArgumentException) { - Log.e("AboutFragment", "Invalid argument for sharing", e) + Log.e("ShareHelper", "Invalid argument for sharing", e) } } @@ -48,23 +52,28 @@ object ShareHelper { * @throws ActivityNotFoundException If no email client is installed. * @throws IllegalArgumentException If there's an issue with the email intent arguments. */ + fun sendEmail(context: Context) { try { - val intent = - Intent(Intent.ACTION_SEND).apply { - type = "message/rfc822" - putExtra(Intent.EXTRA_EMAIL, arrayOf("team@scri.be")) - putExtra(Intent.EXTRA_SUBJECT, "Hey Scribe!") + val intent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf("team@scri.be")) + putExtra(Intent.EXTRA_SUBJECT, "Hey Scribe!") + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - ContextCompat.startActivity( - context, - Intent.createChooser(intent, "Choose an Email client:"), - null, - ) + } + + val chooser = Intent.createChooser(intent, "Choose an Email client:") + if (context !is Activity) { + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + context.startActivity(chooser) } catch (e: ActivityNotFoundException) { - Log.e("AboutFragment", "No email client found", e) + Log.e("ShareHelper", "No email client found", e) } catch (e: IllegalArgumentException) { - Log.e("AboutFragment", "Invalid argument for sending email", e) + Log.e("ShareHelper", "Invalid argument for sending email", e) } } } From 9832e0efaead1b4dce6116e910e49297ef9745db Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 18:17:39 +0100 Subject: [PATCH 08/13] fix class to follow kotlin standard formatting --- .../java/be/scri/helpers/ui/ShareHelper.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt b/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt index fe7537512..5847669e4 100644 --- a/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt +++ b/app/src/main/java/be/scri/helpers/ui/ShareHelper.kt @@ -21,13 +21,14 @@ object ShareHelper { */ fun shareScribe(context: Context) { try { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "https://github.com/scribe-org/Scribe-Android") - if (context !is Activity) { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val intent = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "https://github.com/scribe-org/Scribe-Android") + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } } - } val chooser = Intent.createChooser(intent, "Share via") if (context !is Activity) { @@ -55,14 +56,15 @@ object ShareHelper { fun sendEmail(context: Context) { try { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "message/rfc822" - putExtra(Intent.EXTRA_EMAIL, arrayOf("team@scri.be")) - putExtra(Intent.EXTRA_SUBJECT, "Hey Scribe!") - if (context !is Activity) { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val intent = + Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf("team@scri.be")) + putExtra(Intent.EXTRA_SUBJECT, "Hey Scribe!") + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } } - } val chooser = Intent.createChooser(intent, "Choose an Email client:") if (context !is Activity) { From 7ceb7cba557320cb6215f43139c75a8b66a0239e Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 20:03:50 +0100 Subject: [PATCH 09/13] Fix NoActivityResumedException in onShareScribeClick test by launching MainActivity --- .../be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt index eed93533d..9438f0387 100644 --- a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt @@ -43,9 +43,10 @@ class AboutUtilInstrumentedTest { @Test fun test_onShareScribeClick_doesNotCrash() { // Use the app context - val context = InstrumentationRegistry.getInstrumentation().targetContext - - AboutUtil.onShareScribeClick(context) + val scenario = ActivityScenario.launch(MainActivity::class.java) + scenario.onActivity { activity -> + AboutUtil.onShareScribeClick(activity) + } Intents.intended(hasAction(Intent.ACTION_CHOOSER)) } From 9e1d2f7a18c2b73729f0bfddef0e266cbf37627b Mon Sep 17 00:00:00 2001 From: Akindele Michael Date: Sun, 22 Jun 2025 20:18:39 +0100 Subject: [PATCH 10/13] docs: add KDoc for AboutUtilInstrumentedTest and its test methods --- .../about/AboutUtilInstrumentedTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt index 9438f0387..aa8feaf33 100644 --- a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt @@ -25,21 +25,42 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +/** + * Instrumented tests for verifying the behavior of [AboutUtil] functions within an Android context. + * + * This class tests that various utility functions used in the About screen: + * - Trigger appropriate Intents + * - Handle context properly + * - Return expected list data + * - Execute callbacks correctly + * + * It uses Espresso Intents, ActivityScenario, and Compose test utilities. + */ @RunWith(AndroidJUnit4::class) class AboutUtilInstrumentedTest { @get:Rule val composeTestRule = createComposeRule() + /** + * Initializes Espresso Intents before each test. + */ @Before fun setup() { Intents.init() } + /** + * Releases Espresso Intents after each test to avoid leaks. + */ @After fun tearDown() { Intents.release() } + /** + * Verifies that [AboutUtil.onShareScribeClick] does not crash when called + * and launches a share chooser intent from an Activity context. + */ @Test fun test_onShareScribeClick_doesNotCrash() { // Use the app context @@ -51,6 +72,10 @@ class AboutUtilInstrumentedTest { Intents.intended(hasAction(Intent.ACTION_CHOOSER)) } + /** + * Verifies that [AboutUtil.onRateScribeClick] executes without crashing + * when invoked from an Activity context. + */ @Test fun test_onRateScribeClick_doesNotCrash() { val scenario = ActivityScenario.launch(MainActivity::class.java) @@ -59,6 +84,9 @@ class AboutUtilInstrumentedTest { } } + /** + * Asserts that [AboutUtil.onMailClick] launches an email intent wrapped in a chooser. + */ @Test fun test_onMailClick_launchesEmailIntent() { val context = InstrumentationRegistry.getInstrumentation().targetContext @@ -80,6 +108,10 @@ class AboutUtilInstrumentedTest { ) } + /** + * Tests [AboutUtil.getCommunityList] returns a valid list and + * triggers correct callbacks for Share and Wikimedia items. + */ @Test fun testGetCommunityList() { println("Testing getCommunityList...") @@ -138,6 +170,10 @@ class AboutUtilInstrumentedTest { println("getCommunityList test passed!") } + /** + * Tests [AboutUtil.getFeedbackAndSupportList] returns a valid list and + * executes all related click callbacks correctly. + */ @Test fun testGetFeedbackAndSupportList() { println("Testing getFeedbackAndSupportList...") @@ -200,6 +236,10 @@ class AboutUtilInstrumentedTest { println("getFeedbackAndSupportList test passed!") } + /** + * Tests [AboutUtil.getLegalListItems] returns legal items and triggers + * callbacks for privacy policy and licenses. + */ @Test fun testGetLegalListItems() { println("Testing getLegalListItems...") @@ -255,6 +295,9 @@ class AboutUtilInstrumentedTest { println("getLegalListItems test passed!") } + /** + * Asserts that all [ExternalLinks] constants are correctly defined and non-empty. + */ @Test fun testExternalLinksConstants() { println("Testing ExternalLinks constants...") From e7dfa9f348d5fd31c75ecd7fa49bfa4d3581d2de Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Tue, 24 Jun 2025 22:45:02 +0530 Subject: [PATCH 11/13] chore:formating the build.gradle.kts --- app/build.gradle.kts | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 54094cc3b..61301c780 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ plugins { id("org.jmailen.kotlinter") id("io.gitlab.arturbosch.detekt") id("com.google.devtools.ksp") version "2.0.0-1.0.22" apply true -// id("de.mannodermaus.android-junit5") version "1.11.2.0" - id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" + id("de.mannodermaus.android-junit5") version "1.11.2.0" + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" id("jacoco") kotlin("plugin.serialization") version "1.9.0" id("org.jetbrains.kotlinx.kover") version "0.6.1" @@ -216,7 +216,7 @@ dependencies { // ========================== // Kotlin // ========================== - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.21") // ========================== // Layout and UI @@ -259,32 +259,27 @@ dependencies { // ========================== // Unit Testing // ========================== - - testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") testImplementation("io.mockk:mockk:$mockkVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") testImplementation("junit:junit:4.13.2") testImplementation("org.robolectric:robolectric:4.13") - testImplementation("androidx.compose.ui:ui-test-junit4:1.8.0") // Compose UI Testing for unit tests + testImplementation("androidx.compose.ui:ui-test-junit4:1.8.3") testImplementation("org.jetbrains.kotlin:kotlin-test:2.0.20") testImplementation ("org.mockito:mockito-core:5.12.0") testImplementation ("org.mockito:mockito-inline:5.2.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") testImplementation("com.google.truth:truth:1.4.4") -// testImplementation ("org.mockito:mockito-junit-jupiter:5.12.0") - // For Instrumentation Tests - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.0") - androidTestImplementation("io.mockk:mockk-android:$mockkVersion") - androidTestImplementation("com.google.truth:truth:1.4.4") - debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.5") - // ========================== // UI Tests // ========================== + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.3") + androidTestImplementation("io.mockk:mockk-android:$mockkVersion") + androidTestImplementation("com.google.truth:truth:1.4.4") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.8.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // ========================== @@ -295,21 +290,20 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1") androidTestImplementation("androidx.test.ext:junit:1.2.1") androidTestImplementation("androidx.test:runner:1.6.2") - androidTestImplementation("io.mockk:mockk-android:1.13.13") + androidTestImplementation("io.mockk:mockk-android:1.14.2") // ========================== // Other // ========================== - - api("joda-time:joda-time:2.12.7") + api("joda-time:joda-time:2.14.0") api("com.github.tibbi:RecyclerView-FastScroller:e7d3e150c4") api("com.github.tibbi:reprint:2cb206415d") api("androidx.core:core-ktx:1.16.0") - api("com.google.code.gson:gson:2.11.0") + api("com.google.code.gson:gson:2.13.1") api("com.github.bumptech.glide:glide:4.16.0") - ksp("com.github.bumptech.glide:ksp:4.14.2") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + ksp("com.github.bumptech.glide:ksp:4.16.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") } tasks.register("moveFromi18n") { @@ -353,9 +347,6 @@ tasks.withType(Test::class) { } } -//tasks.withType { -// useJUnitPlatform() -//} tasks.register("jacocoTestReport") { group = "Reporting" From 1416b72276b860f4e30907102c380867e0dc1ba4 Mon Sep 17 00:00:00 2001 From: angrezichatterbox Date: Tue, 24 Jun 2025 22:51:24 +0530 Subject: [PATCH 12/13] chore:migrate from junit4 to junit4 for unit tests --- app/src/test/kotlin/be/scri/helpers/PreferencesHelperTest.kt | 4 ++-- app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt | 4 ++-- app/src/test/kotlin/helpers/ComprehensiveCoverageTest.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/test/kotlin/be/scri/helpers/PreferencesHelperTest.kt b/app/src/test/kotlin/be/scri/helpers/PreferencesHelperTest.kt index 7b96ec8b8..96933c7db 100644 --- a/app/src/test/kotlin/be/scri/helpers/PreferencesHelperTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/PreferencesHelperTest.kt @@ -6,8 +6,8 @@ package be.scri.helpers -import org.junit.Assert.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test class PreferencesHelperTest { @Test diff --git a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt index f93e2d2d6..5365abafb 100644 --- a/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt +++ b/app/src/test/kotlin/helpers/AlphanumericComparatorTest.kt @@ -7,8 +7,8 @@ package helpers import be.scri.helpers.AlphanumericComparator -import org.junit.Assert.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test class AlphanumericComparatorTest { private val subject = AlphanumericComparator() diff --git a/app/src/test/kotlin/helpers/ComprehensiveCoverageTest.kt b/app/src/test/kotlin/helpers/ComprehensiveCoverageTest.kt index 266c64f2a..f3bce1d7a 100644 --- a/app/src/test/kotlin/helpers/ComprehensiveCoverageTest.kt +++ b/app/src/test/kotlin/helpers/ComprehensiveCoverageTest.kt @@ -7,7 +7,7 @@ import android.provider.SyncStateContract.Constants import androidx.test.ext.junit.runners.AndroidJUnit4 import be.scri.activities.MainActivity import be.scri.helpers.AlphanumericComparator -import org.junit.Test +import org.junit.jupiter.api.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) From 1d490e92f1fe7e756ba73829fc3bf17212c53009 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Tue, 24 Jun 2025 19:35:23 +0200 Subject: [PATCH 13/13] Standardize comments in testing file --- .../about/AboutUtilInstrumentedTest.kt | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt index aa8feaf33..e8a9956d9 100644 --- a/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/be/scri/ui/screens/about/AboutUtilInstrumentedTest.kt @@ -63,7 +63,7 @@ class AboutUtilInstrumentedTest { */ @Test fun test_onShareScribeClick_doesNotCrash() { - // Use the app context + // Use the app context. val scenario = ActivityScenario.launch(MainActivity::class.java) scenario.onActivity { activity -> AboutUtil.onShareScribeClick(activity) @@ -129,11 +129,11 @@ class AboutUtilInstrumentedTest { context = context, ) - // Test list is not empty + // Test list is not empty. assertThat(communityList.items).isNotEmpty() assertThat(communityList.items).hasSize(5) - // Test each item has required fields + // Test each item has required fields. communityList.items.forEach { item -> assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) val linkItem = item as ScribeItem.ExternalLinkItem @@ -144,7 +144,7 @@ class AboutUtilInstrumentedTest { assertThat(linkItem.onClick).isNotNull() } - // Test specific items + // Test specific items. val githubItem = communityList.items[0] as ScribeItem.ExternalLinkItem assertThat(githubItem.leadingIcon).isEqualTo(R.drawable.github_logo) assertThat(githubItem.title).isEqualTo(R.string.app_about_community_github) @@ -157,13 +157,13 @@ class AboutUtilInstrumentedTest { assertThat(wikimediaItem.leadingIcon).isEqualTo(R.drawable.wikimedia_logo_black) assertThat(wikimediaItem.title).isEqualTo(R.string.app_about_community_wikimedia) - // Test onClick callbacks + // Test onClick callbacks. shareItem.onClick() wikimediaItem.onClick() } } - // Verify callbacks were triggered + // Verify callbacks were triggered. assertThat(shareClicked).isTrue() assertThat(wikimediaClicked).isTrue() @@ -193,11 +193,11 @@ class AboutUtilInstrumentedTest { context = context, ) - // Test list is not empty + // Test list is not empty. assertThat(feedbackList.items).isNotEmpty() assertThat(feedbackList.items).hasSize(5) - // Test each item has required fields + // Test each item has required fields. feedbackList.items.forEach { item -> assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) val linkItem = item as ScribeItem.ExternalLinkItem @@ -208,7 +208,7 @@ class AboutUtilInstrumentedTest { assertThat(linkItem.onClick).isNotNull() } - // Test specific items + // Test specific items. val rateItem = feedbackList.items[0] as ScribeItem.ExternalLinkItem assertThat(rateItem.leadingIcon).isEqualTo(R.drawable.star) assertThat(rateItem.title).isEqualTo(R.string.app_about_feedback_rate_scribe) @@ -221,14 +221,14 @@ class AboutUtilInstrumentedTest { assertThat(hintsItem.leadingIcon).isEqualTo(R.drawable.light_bulb_icon) assertThat(hintsItem.title).isEqualTo(R.string.app_about_feedback_app_hints) - // Test onClick callbacks + // Test onClick callbacks. rateItem.onClick() mailItem.onClick() hintsItem.onClick() } } - // Verify callbacks were triggered + // Verify callbacks were triggered. assertThat(rateClicked).isTrue() assertThat(mailClicked).isTrue() assertThat(resetHintsClicked).isTrue() @@ -256,11 +256,11 @@ class AboutUtilInstrumentedTest { onThirdPartyLicensesClick = { thirdPartyLicensesClicked = true }, ) - // Test list is not empty + // Test list is not empty. assertThat(legalList.items).isNotEmpty() assertThat(legalList.items).hasSize(2) - // Test each item has required fields + // Test each item has required fields. legalList.items.forEach { item -> assertThat(item).isInstanceOf(ScribeItem.ExternalLinkItem::class.java) val linkItem = item as ScribeItem.ExternalLinkItem @@ -271,7 +271,7 @@ class AboutUtilInstrumentedTest { assertThat(linkItem.onClick).isNotNull() } - // Test specific items + // Test specific items. val privacyItem = legalList.items[0] as ScribeItem.ExternalLinkItem assertThat(privacyItem.leadingIcon).isEqualTo(R.drawable.shield_lock) assertThat(privacyItem.title).isEqualTo(R.string.app_about_legal_privacy_policy) @@ -282,13 +282,13 @@ class AboutUtilInstrumentedTest { assertThat(licenseItem.title).isEqualTo(R.string.app_about_legal_third_party) assertThat(licenseItem.trailingIcon).isEqualTo(R.drawable.right_arrow) - // Test onClick callbacks + // Test onClick callbacks. privacyItem.onClick() licenseItem.onClick() } } - // Verify callbacks were triggered + // Verify callbacks were triggered. assertThat(privacyPolicyClicked).isTrue() assertThat(thirdPartyLicensesClicked).isTrue() @@ -302,7 +302,7 @@ class AboutUtilInstrumentedTest { fun testExternalLinksConstants() { println("Testing ExternalLinks constants...") - // Test that external links are properly defined + // Test that external links are properly defined. assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_SCRIBE).isNotEmpty() assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_ISSUES).isNotEmpty() assertThat(be.scri.ui.screens.about.ExternalLinks.GITHUB_RELEASES).isNotEmpty()