diff --git a/.github/workflows/manual_deploy_to_firebase_workflow.yml b/.github/workflows/manual_deploy_to_firebase_workflow.yml
new file mode 100644
index 0000000..316fb7f
--- /dev/null
+++ b/.github/workflows/manual_deploy_to_firebase_workflow.yml
@@ -0,0 +1,68 @@
+name: Deploy Android Debug
+on:
+ workflow_dispatch:
+ inputs:
+ versionCode:
+ description: Version Code (eg. 123)
+ required: true
+ versionName:
+ description: Version Name (eg 1.2.3)
+ required: true
+
+env:
+ VERSION_CODE: ${{ github.event.inputs.versionCode }}
+ VERSION_NAME: ${{ github.event.inputs.versionName }}
+
+jobs:
+
+ build:
+ name: 🔨 Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: adopt
+ java-version: "17"
+
+ - name: Make gradle executable
+ run: chmod +x ./gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew build --stacktrace
+
+ deploy:
+ name: 🚀 Deploy to Firebase App Distribution
+ needs:
+ - build
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: adopt
+ java-version: "17"
+
+ - name: Make gradle executable
+ run: chmod +x ./gradlew
+
+ - name: Get local.properties from secrets
+ run: echo "${{secrets.ProjectSecrets }}" > $GITHUB_WORKSPACE/local.properties
+
+ - name: Build Debug APK
+ run: ./gradlew assembleDebug --stacktrace
+
+ - name: Firebase App Distribution
+ uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0
+ with:
+ appId: ${{secrets.FIREBASE_APP_ID}}
+ serviceCredentialsFileContent: ${{secrets.CREDENTIAL_FILE_CONTENT}}
+ groups: testers
+ file: app/build/outputs/apk/debug/app-debug.apk
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
new file mode 100644
index 0000000..74780fc
--- /dev/null
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 44794e9..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,10 +1,6 @@
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..e80ef22
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml
new file mode 100644
index 0000000..02b915b
--- /dev/null
+++ b/.idea/git_toolbox_prj.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d3cddf7..141199a 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,6 +1,5 @@
-
@@ -40,4 +39,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..44ca2d9
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml
new file mode 100644
index 0000000..bee5678
--- /dev/null
+++ b/.idea/ktlint-plugin.xml
@@ -0,0 +1,6 @@
+
+
+
+ DISTRACT_FREE
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..b1b87d7
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 61576aa..d45b404 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -50,6 +50,9 @@ dependencies {
implementation(projects.core.domain)
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.analytics)
+ // For Robolectric tests.
+ testImplementation(libs.hilt.android.testing)
+ kspTest(libs.hilt.compiler)
}
sentry {
diff --git a/app/src/main/java/com/devmike/gitissuesmobile/navigation/AppNavigation.kt b/app/src/main/java/com/devmike/gitissuesmobile/navigation/AppNavigation.kt
index 85c3e15..7020600 100644
--- a/app/src/main/java/com/devmike/gitissuesmobile/navigation/AppNavigation.kt
+++ b/app/src/main/java/com/devmike/gitissuesmobile/navigation/AppNavigation.kt
@@ -17,7 +17,9 @@ fun AppNavigation(
startDestination = com.devmike.domain.appdestinations.AppDestinations.RepositorySearch,
) {
composable {
- RepositorySearchScreen(onLogoutClicked = onLogout) { name, owner ->
+ RepositorySearchScreen(
+ onLogoutClicked = onLogout,
+ ) { name, owner ->
val destination =
com.devmike.domain.appdestinations.AppDestinations
.Issues(repository = name, owner = owner)
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index 1b1969c..cf9e0de 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -13,8 +13,8 @@ android {
dependencies {
api(projects.core.domain)
- implementation(projects.core.network)
- implementation(projects.core.database)
+ api(projects.core.network)
+ api(projects.core.database)
testImplementation(libs.androidx.test.ext)
testImplementation(libs.mockk)
testImplementation(libs.turbine)
diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts
index f5091eb..39828ae 100644
--- a/core/testing/build.gradle.kts
+++ b/core/testing/build.gradle.kts
@@ -9,7 +9,8 @@ android {
}
dependencies {
-
+ implementation(projects.core.data)
+ implementation(projects.core.datastore)
implementation(libs.androidx.arch.core.testing)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.core.ktx)
diff --git a/feature/repository/build.gradle.kts b/feature/repository/build.gradle.kts
index 6582039..81cdcd7 100644
--- a/feature/repository/build.gradle.kts
+++ b/feature/repository/build.gradle.kts
@@ -20,13 +20,16 @@ dependencies {
implementation(projects.core.data)
implementation(projects.core.domain)
+ debugImplementation(libs.ui.test.manifest)
implementation(libs.androidx.paging.compose)
implementation(libs.coil.compose)
testImplementation(libs.mockk)
testImplementation(libs.turbine)
testImplementation(libs.androidx.arch.core.testing)
+ androidTestImplementation(libs.androidx.arch.core.testing)
testImplementation(libs.kotlinx.coroutines.test)
+ androidTestImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.core.ktx)
testImplementation(libs.truth)
// alternatively - without Android dependencies for tests
diff --git a/feature/repository/src/androidTest/AndroidManifest.xml b/feature/repository/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..2caf7c3
--- /dev/null
+++ b/feature/repository/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchScreen.kt b/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchScreen.kt
index 98ec31a..83ce08f 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchScreen.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchScreen.kt
@@ -12,12 +12,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems
@@ -33,6 +35,9 @@ fun RepositorySearchScreen(
onLogoutClicked: () -> Unit,
onRepositoryClick: (name: String, owner: String) -> Unit,
) {
+ LaunchedEffect(viewModel) {
+ viewModel.modifyDebounceTime(DEBOUNCE_DURATION)
+ }
val repositoriesState = viewModel.searchResults.collectAsLazyPagingItems()
val showIdleScreen =
@@ -64,7 +69,10 @@ fun RepositorySearchScreen(
)
},
actions = {
- IconButton(onClick = { showLogoutDialog = true }) {
+ IconButton(
+ onClick = { showLogoutDialog = true },
+ modifier = Modifier.testTag("logout_button"),
+ ) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = "logout",
@@ -81,7 +89,7 @@ fun RepositorySearchScreen(
IdleScreen(modifier = Modifier.padding(paddingValues))
} else {
RepositoryListScreen(
- modifier = Modifier.padding(paddingValues),
+ modifier = Modifier.padding(paddingValues).testTag("repositorieslist"),
repositoriesState = repositoriesState,
onRepositoryClick = onRepositoryClick,
)
@@ -91,3 +99,4 @@ fun RepositorySearchScreen(
}
const val MINIMUM_SEARCH_LENGTH = 3
+const val DEBOUNCE_DURATION = 500L
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchViewModel.kt b/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchViewModel.kt
index 8a65c76..9a190f7 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchViewModel.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/RepositorySearchViewModel.kt
@@ -6,7 +6,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.devmike.domain.repository.IRepoSearchRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -15,7 +14,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@@ -24,7 +23,7 @@ class RepositorySearchViewModel
constructor(
private val repoSearchRepository: IRepoSearchRepository,
) : ViewModel() {
- private val debounceDuration = 500L
+ private var debounceDuration = 0L
var searchQuery by mutableStateOf("")
private set
@@ -36,19 +35,24 @@ class RepositorySearchViewModel
(
if (query.isEmpty()) {
- flowOf(PagingData.empty())
+ flowOf()
} else {
- repoSearchRepository.searchRepositories(query.trim())
+
+ val data = repoSearchRepository.searchRepositories(query.trim())
+
+ data.cachedIn(viewModelScope)
}
+
)
}.cachedIn(viewModelScope)
- .stateIn(
- viewModelScope,
- kotlinx.coroutines.flow.SharingStarted.Lazily,
- PagingData.empty(),
- )
fun modifySearchQuery(query: String) {
- searchQuery = query
+ viewModelScope.launch {
+ searchQuery = query
+ }
+ }
+
+ fun modifyDebounceTime(duration: Long) {
+ debounceDuration = duration
}
}
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/components/LogoutConfirmationDialog.kt b/feature/repository/src/main/java/com/devmike/repository/screen/components/LogoutConfirmationDialog.kt
index 6a1d1e4..90435d8 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/components/LogoutConfirmationDialog.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/components/LogoutConfirmationDialog.kt
@@ -8,7 +8,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
@Composable
@@ -32,6 +34,7 @@ fun LogoutConfirmationDialog(
Text("Keep me logged in")
}
},
+ modifier = Modifier.testTag("Logout_Dialog"),
)
}
}
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/components/NoRepositoriesFoundScreen.kt b/feature/repository/src/main/java/com/devmike/repository/screen/components/NoRepositoriesFoundScreen.kt
index 51c0379..f55c4dd 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/components/NoRepositoriesFoundScreen.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/components/NoRepositoriesFoundScreen.kt
@@ -32,7 +32,7 @@ fun NoRepositoriesFoundScreen() {
) {
Image(
painter = painterResource(id = R.drawable.not_found),
- contentDescription = "No repositoriesfound",
+ contentDescription = "No repositories found",
modifier = Modifier.size(120.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
)
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryItem.kt b/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryItem.kt
index 5425880..69e340a 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryItem.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryItem.kt
@@ -22,18 +22,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.devmike.domain.models.RepositoryModel
-import timber.log.Timber
@Composable
fun RepositoryItem(
@@ -42,11 +41,8 @@ fun RepositoryItem(
) {
val context = LocalContext.current
- LaunchedEffect(key1 = repository) {
- Timber.tag("repoScreen").d("$repository")
- }
Card(
- modifier = Modifier.fillMaxWidth().padding(4.dp),
+ modifier = Modifier.fillMaxWidth().padding(4.dp).testTag("repository_item"),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
onClick = {
if (repository.issueCount >
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryListScreen.kt b/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryListScreen.kt
index 7b12b70..6c2284c 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryListScreen.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/components/RepositoryListScreen.kt
@@ -17,12 +17,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
-import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.devmike.domain.models.RepositoryModel
-import kotlinx.coroutines.flow.flow
+import com.devmike.repository.utils.createTestPagingFlow
@Composable
fun RepositoryListScreen(
@@ -34,14 +33,14 @@ fun RepositoryListScreen(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- items(
+ /* items(
repositoriesState.itemCount,
key = repositoriesState.itemKey { it.url },
) { count ->
repositoriesState[count]?.let {
RepositoryItem(repository = it, onRepositoryClick)
}
- }
+ }*/
repositoriesState.apply {
when {
loadState.refresh is LoadState.NotLoading -> {
@@ -129,85 +128,76 @@ fun RepositoryListScreen(
@Preview
fun RepositoryListScreenPreview() {
val items =
- flow {
-// emit(PagingData.empty(LoadStates(
-// refresh = LoadState.Loading,
-// prepend = LoadState.NotLoading(false),
-// append = LoadState.NotLoading(false)
-// )))
-//
-// kotlinx.coroutines.delay(2000L)
- emit(
- PagingData.from(
- listOf(
- RepositoryModel(
- url = "https://github.com/square/retrofit",
- name = "Retrofit",
- nameWithOwner = "square/retrofit",
- owner = "square",
- description = "A type-safe HTTP client for Android and Java.",
- stargazers = 40000,
- forkCount = 8000, issueCount = 1500,
- avatarUrl = "https://avatars.githubusercontent.com/u/82592",
- ),
- RepositoryModel(
- url = "https://github.com/bumptech/glide",
- name = "Glide",
- nameWithOwner = "bumptech/glide",
- owner = "bumptech",
- description =
- """An image loading and caching library
+
+ createTestPagingFlow(
+ listOf(
+ RepositoryModel(
+ url = "https://github.com/square/retrofit",
+ name = "Retrofit",
+ nameWithOwner = "square/retrofit",
+ owner = "square",
+ description = "A type-safe HTTP client for Android and Java.",
+ stargazers = 40000,
+ forkCount = 8000, issueCount = 1500,
+ avatarUrl = "https://avatars.githubusercontent.com/u/82592",
+ ),
+ RepositoryModel(
+ url = "https://github.com/bumptech/glide",
+ name = "Glide",
+ nameWithOwner = "bumptech/glide",
+ owner = "bumptech",
+ description =
+ """An image loading and caching library
|for Android focused on smooth scrolling.
- """.trimMargin(),
- stargazers = 35000,
- forkCount = 6500,
- issueCount = 1200,
- avatarUrl = "https://avatars.githubusercontent.com/u/4255330",
- ),
- RepositoryModel(
- url = "https://github.com/JetBrains/kotlin",
- name = "Kotlin",
- nameWithOwner = "JetBrains/kotlin",
- owner = "JetBrains",
- description = "The Kotlin Programming Language.",
- stargazers = 41000,
- forkCount = 7800,
- issueCount = 2300,
- avatarUrl = "https://avatars.githubusercontent.com/u/878438",
- ),
- RepositoryModel(
- url = "https://github.com/androidx/androidx",
- name = "AndroidX",
- nameWithOwner = "androidx/androidx",
- owner = "androidx",
- description =
- """
- AndroidX is the open-source project that the Android team uses to
- develop, test, package, version and release libraries within Jetpack.
- """.trimIndent(),
- stargazers = 10000,
- forkCount = 3500, issueCount = 800,
- avatarUrl = "https://avatars.githubusercontent.com/u/44576557",
- ),
- RepositoryModel(
- url = "https://github.com/coil-kt/coil",
- name = "Coil",
- nameWithOwner = "coil-kt/coil",
- owner = "coil-kt",
- description =
- """
- "An image loading library for
- Android backed by Kotlin Coroutines."
- """.trimIndent(),
- stargazers = 8000,
- forkCount = 1000,
- issueCount = 300,
- avatarUrl = "https://avatars.githubusercontent.com/u/70237063",
- ),
- ),
+ """.trimMargin(),
+ stargazers = 35000,
+ forkCount = 6500,
+ issueCount = 1200,
+ avatarUrl = "https://avatars.githubusercontent.com/u/4255330",
),
- )
- }
+ RepositoryModel(
+ url = "https://github.com/JetBrains/kotlin",
+ name = "Kotlin",
+ nameWithOwner = "JetBrains/kotlin",
+ owner = "JetBrains",
+ description = "The Kotlin Programming Language.",
+ stargazers = 41000,
+ forkCount = 7800,
+ issueCount = 2300,
+ avatarUrl = "https://avatars.githubusercontent.com/u/878438",
+ ),
+ RepositoryModel(
+ url = "https://github.com/androidx/androidx",
+ name = "AndroidX",
+ nameWithOwner = "androidx/androidx",
+ owner = "androidx",
+ description =
+ """
+ AndroidX is the open-source project that the Android team uses to
+ develop, test, package, version and release libraries within Jetpack.
+ """.trimIndent(),
+ stargazers = 10000,
+ forkCount = 3500, issueCount = 800,
+ avatarUrl = "https://avatars.githubusercontent.com/u/44576557",
+ ),
+ RepositoryModel(
+ url = "https://github.com/coil-kt/coil",
+ name = "Coil",
+ nameWithOwner = "coil-kt/coil",
+ owner = "coil-kt",
+ description =
+ """
+ "An image loading library for
+ Android backed by Kotlin Coroutines."
+ """.trimIndent(),
+ stargazers = 8000,
+ forkCount = 1000,
+ issueCount = 300,
+ avatarUrl = "https://avatars.githubusercontent.com/u/70237063",
+ ),
+ ),
+ )
+
RepositoryListScreen(
modifier = Modifier,
repositoriesState = items.collectAsLazyPagingItems(),
diff --git a/feature/repository/src/main/java/com/devmike/repository/screen/components/SearchTextField.kt b/feature/repository/src/main/java/com/devmike/repository/screen/components/SearchTextField.kt
index d4cc86d..b15106e 100644
--- a/feature/repository/src/main/java/com/devmike/repository/screen/components/SearchTextField.kt
+++ b/feature/repository/src/main/java/com/devmike/repository/screen/components/SearchTextField.kt
@@ -23,6 +23,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -50,7 +51,8 @@ fun SearchTextField(
modifier
.fillMaxWidth()
.focusRequester(requester)
- .padding(horizontal = 4.dp),
+ .padding(horizontal = 4.dp)
+ .testTag("search_repositories"),
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
diff --git a/feature/repository/src/main/java/com/devmike/repository/utils/TestPagingScource.kt b/feature/repository/src/main/java/com/devmike/repository/utils/TestPagingScource.kt
new file mode 100644
index 0000000..3e9e8e2
--- /dev/null
+++ b/feature/repository/src/main/java/com/devmike/repository/utils/TestPagingScource.kt
@@ -0,0 +1,26 @@
+package com.devmike.repository.utils
+
+import androidx.paging.LoadState
+import androidx.paging.LoadStates
+import androidx.paging.PagingData
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+fun createTestPagingFlow(
+ items: List,
+ refreshState: LoadState = LoadState.NotLoading(endOfPaginationReached = false),
+ appendState: LoadState = LoadState.NotLoading(endOfPaginationReached = false),
+ prependState: LoadState = LoadState.NotLoading(endOfPaginationReached = false),
+): Flow> {
+ return flowOf(
+ PagingData.from(
+ data = items,
+ sourceLoadStates =
+ LoadStates(
+ refresh = refreshState,
+ append = appendState,
+ prepend = prependState,
+ ),
+ ),
+ )
+}
diff --git a/feature/repository/src/test/java/com/devmike/repository/SearchViewModelTest.kt b/feature/repository/src/test/java/com/devmike/repository/SearchViewModelTest.kt
index 769e336..e1ec057 100644
--- a/feature/repository/src/test/java/com/devmike/repository/SearchViewModelTest.kt
+++ b/feature/repository/src/test/java/com/devmike/repository/SearchViewModelTest.kt
@@ -65,6 +65,7 @@ class SearchViewModelTest {
// When search query is modified
viewModel.modifySearchQuery(query)
+ advanceUntilIdle()
// Then searchQuery should be updated
Truth.assertThat(viewModel.searchQuery).isEqualTo(query)
diff --git a/feature/repository/src/test/java/com/devmike/repository/testdouble/RepositorySearchDouble.kt b/feature/repository/src/test/java/com/devmike/repository/testdouble/RepositorySearchDouble.kt
new file mode 100644
index 0000000..ac7d04b
--- /dev/null
+++ b/feature/repository/src/test/java/com/devmike/repository/testdouble/RepositorySearchDouble.kt
@@ -0,0 +1,148 @@
+package com.devmike.repository.testdouble
+
+import androidx.paging.PagingData
+import com.devmike.domain.models.RepositoryModel
+import com.devmike.domain.repository.IRepoSearchRepository
+import com.devmike.repository.utils.createTestPagingFlow
+import kotlinx.coroutines.flow.Flow
+
+class RepositorySearchDouble : IRepoSearchRepository {
+ private val fakeRepositories: List
+ get() = fakeFlutterRepositories + fakeRustRepositories
+
+ override fun searchRepositories(query: String): Flow> {
+ val items = fakeRepositories.filter { it.nameWithOwner.contains(query, ignoreCase = true) }
+
+ return createTestPagingFlow(items)
+ }
+}
+
+val fakeFlutterRepositories =
+ listOf(
+ RepositoryModel(
+ url = "https://github.com/flutter/flutter",
+ name = "flutter",
+ nameWithOwner = "flutter/flutter",
+ owner = "flutter",
+ description =
+ "Flutter makes it easy and fast to " +
+ "build beautiful apps for mobile and beyond",
+ stargazers = 165315,
+ forkCount = 27277,
+ issueCount = 99037,
+ avatarUrl = "https://avatars.githubusercontent.com/u/14101776?v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/flutter/plugins",
+ name = "plugins",
+ nameWithOwner = "flutter/plugins",
+ owner = "flutter",
+ description = "Plugins for Flutter maintained by the Flutter team",
+ stargazers = 17467,
+ forkCount = 9806,
+ issueCount = 0,
+ avatarUrl = "https://avatars.githubusercontent.com/u/14101776?v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/iampawan/FlutterExampleApps",
+ name = "FlutterExampleApps",
+ nameWithOwner = "iampawan/FlutterExampleApps",
+ owner = "iampawan",
+ description = "[Example APPS] Basic Flutter apps, for flutter devs.",
+ stargazers = 20489,
+ forkCount = 3768,
+ issueCount = 41,
+ avatarUrl =
+ "https://avatars.githubusercontent.com/u" +
+ "/12619420?u=a49ba4b7f5ae93afc2febc86a021b42d2f5b5858&v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/Solido/awesome-flutter",
+ name = "awesome-flutter",
+ nameWithOwner = "Solido/awesome-flutter",
+ owner = "Solido",
+ description =
+ "An awesome list that curates the best Flutter libraries," +
+ " tools, tutorials, articles and more.",
+ stargazers = 53256,
+ forkCount = 6647,
+ issueCount = 0,
+ avatarUrl =
+ "https://avatars.githubusercontent.com/u/" +
+ "1295961?u=b85eaeb98c4c24aeec8046b7839a5ddf2d504289&v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/flutter/engine",
+ name = "engine",
+ nameWithOwner = "flutter/engine",
+ owner = "flutter",
+ description = "The Flutter engine",
+ stargazers = 7376,
+ forkCount = 5948,
+ issueCount = 0,
+ avatarUrl = "https://avatars.githubusercontent.com/u/14101776?v=4",
+ ),
+ )
+
+val fakeRustRepositories =
+ listOf(
+ RepositoryModel(
+ url = "https://github.com/rust-lang/rust",
+ name = "rust",
+ nameWithOwner = "rust-lang/rust",
+ owner = "rust-lang",
+ description = "Empowering everyone to build reliable and efficient software.",
+ stargazers = 97402,
+ forkCount = 12591,
+ issueCount = 54709,
+ avatarUrl = "https://avatars.githubusercontent.com/u/5430905?v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/TheAlgorithms/Rust",
+ name = "Rust",
+ nameWithOwner = "TheAlgorithms/Rust",
+ owner = "TheAlgorithms",
+ description = " All Algorithms implemented in Rust ",
+ stargazers = 22465,
+ forkCount = 2188,
+ issueCount = 69,
+ avatarUrl = "https://avatars.githubusercontent.com/u/20487725?v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/rustdesk/rustdesk",
+ name = "rustdesk",
+ nameWithOwner = "rustdesk/rustdesk",
+ owner = "rustdesk",
+ description =
+ "An open-source remote desktop application designed for self-hosting," +
+ " as an alternative to TeamViewer.",
+ stargazers = 73719,
+ forkCount = 8945,
+ issueCount = 3071,
+ avatarUrl =
+ "https://avatars.githubusercontent.com/" +
+ "u/71636191?u=fcdfa5bbe724bd4ec02f6c3b2419ff25b7f5eb07&v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/rust-lang/rustlings",
+ name = "rustlings",
+ nameWithOwner = "rust-lang/rustlings",
+ owner = "rust-lang",
+ description = "Small exercises to get you used to reading and writing Rust code!",
+ stargazers = 53227,
+ forkCount = 10061,
+ issueCount = 663,
+ avatarUrl = "https://avatars.githubusercontent.com/u/5430905?v=4",
+ ),
+ RepositoryModel(
+ url = "https://github.com/tensorflow/rust",
+ name = "rust",
+ nameWithOwner = "tensorflow/rust",
+ owner = "tensorflow",
+ description = "Rust language bindings for TensorFlow",
+ stargazers = 5140,
+ forkCount = 422,
+ issueCount = 189,
+ avatarUrl = "https://avatars.githubusercontent.com/u/15658638?v=4",
+ ),
+ )
diff --git a/feature/repository/src/test/java/com/devmike/repository/uitest/RepositorySearchTest.kt b/feature/repository/src/test/java/com/devmike/repository/uitest/RepositorySearchTest.kt
new file mode 100644
index 0000000..e2e6034
--- /dev/null
+++ b/feature/repository/src/test/java/com/devmike/repository/uitest/RepositorySearchTest.kt
@@ -0,0 +1,105 @@
+package com.devmike.repository.uitest
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertAny
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.devmike.domain.repository.IRepoSearchRepository
+import com.devmike.repository.screen.RepositorySearchScreen
+import com.devmike.repository.screen.RepositorySearchViewModel
+import com.devmike.repository.screen.components.RepositoryListScreen
+import com.devmike.repository.testdouble.RepositorySearchDouble
+import com.devmike.repository.testdouble.fakeRustRepositories
+import com.devmike.repository.utils.createTestPagingFlow
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RepositorySearchTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ lateinit var viewModel: RepositorySearchViewModel
+
+ @Before
+ fun setup() {
+ val fakeRepo: IRepoSearchRepository = RepositorySearchDouble()
+ viewModel = RepositorySearchViewModel(fakeRepo)
+ }
+
+ @Test
+ fun `Idle Screen Is Visible when no search query is provided`() {
+ composeTestRule.setContent {
+ RepositorySearchScreen(viewModel, {}, { _, _ -> })
+ }
+
+ composeTestRule.onNodeWithText("Type at least 3 characters to search").assertExists()
+ }
+
+ @Test
+ fun `Search Screen Is Visible when search query is provided`() {
+ viewModel.modifySearchQuery("Android")
+
+ composeTestRule.setContent {
+ RepositorySearchScreen(viewModel, {}, { _, _ -> })
+ }
+
+ composeTestRule.onNodeWithTag("repositorieslist").assertExists()
+ }
+
+ @Test
+ fun repositoryItemsSreVisibleWhenPagingItemsAreProvided() {
+ val items = createTestPagingFlow(fakeRustRepositories)
+
+ composeTestRule.setContent {
+ RepositoryListScreen(
+ modifier = Modifier,
+ repositoriesState = items.collectAsLazyPagingItems(),
+ ) { _, _ ->
+ }
+ }
+
+ composeTestRule
+ .onAllNodesWithTag(
+ "repository_item",
+ ).assertAny(hasText("rust", substring = true))
+ }
+
+ @Test
+ fun `Tapping on logout icon shows the logout dialog`() {
+ composeTestRule.setContent {
+ RepositorySearchScreen(viewModel, {}, { _, _ -> })
+ }
+
+ composeTestRule.onNodeWithTag("logout_button").performClick()
+
+ composeTestRule.onNodeWithTag("Logout_Dialog").assertExists()
+
+ composeTestRule.onNodeWithText("Logout Confirmation").assertExists()
+ }
+
+ @Test
+ fun itemsaredisplayedforacorrectsearchquery() {
+ // viewModel.modifySearchQuery("flutter")
+ composeTestRule.setContent {
+ RepositorySearchScreen(viewModel, {}, { _, _ -> })
+ }
+ composeTestRule.onNodeWithTag("search_repositories").performTextInput("flutter")
+ // Allow time for debounce and collection
+ composeTestRule.mainClock.advanceTimeBy(500)
+
+ composeTestRule
+ .onAllNodesWithTag(
+ "repository_item",
+ ).assertAny(hasText("flutter", substring = true))
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8e38a6e..e64423f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,7 @@ apollo = "4.0.0"
apolloMockserver = "0.0.3"
apolloTestingSupport = "4.0.0"
appauth = "0.11.1"
+hiltAndroidTesting = "2.51.1"
kotlin = "2.0.0"
coreKtx = "1.13.1"
junit = "4.13.2"
@@ -65,6 +66,7 @@ androidx-paging-testing = { module = "androidx.paging:paging-testing", version.r
apollo-mockserver = { module = "com.apollographql.mockserver:apollo-mockserver", version.ref = "apolloMockserver" }
apollo-testing-support = { module = "com.apollographql.apollo:apollo-testing-support", version.ref = "apolloTestingSupport" }
appauth = { module = "net.openid:appauth", version.ref = "appauth" }
+hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hiltAndroidTesting" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -144,11 +146,12 @@ firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashly
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics", version.ref = "firebaseAnalytics" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
+ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
[bundles]
compose = ["androidx-activity-compose","androidx-compose-runtime","androidx-material3","androidx-compose-foundation","androidx-compose-foundation-layout",
"androidx-compose-material-iconsExtended","androidx-compose-ui-util","lottie-compose","androidx-navigation-compose","androidx-ui",
- "androidx-ui-tooling","androidx-ui-tooling-preview","androidx-ui-test-manifest","androidx-hilt-navigation-compose","androidx-lifecycle-viewmodel-compose"]
+ "androidx-ui-tooling","androidx-ui-tooling-preview","androidx-ui-test-manifest","androidx-hilt-navigation-compose","androidx-lifecycle-viewmodel-compose","androidx-ui-test-junit4"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }