Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,11 @@ dependencies {
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.junit.jupiter.params)
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.junit.jupiter)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.turbine)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.espresso.intents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,20 @@ class MainActivityDeeplinkTest {
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText(revision).assertExists()
}

@Test
fun testDeeplink_withAuthorEmail_populatesProfileScreen() {
val email = "[email protected]"
val encodedEmail = "tthibaud%40mozilla.com"
val deeplinkUri = Uri.parse("https://treeherder.mozilla.org/jobs?repo=try&author=$encodedEmail")
val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = deeplinkUri
}

ActivityScenario.launch<MainActivity>(intent).use {
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText(email).assertExists()
}
}
}
24 changes: 24 additions & 0 deletions app/src/main/java/org/mozilla/tryfox/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.mozilla.tryfox.ui.screens.ProfileViewModel
import org.mozilla.tryfox.ui.screens.TryFoxMainScreen
import org.mozilla.tryfox.ui.theme.TryFoxTheme
import java.io.File
import java.net.URLDecoder

sealed class NavScreen(val route: String) {
data object Home : NavScreen("home")
Expand All @@ -36,6 +37,7 @@ sealed class NavScreen(val route: String) {
fun createRoute(project: String, revision: String) = "treeherder_search/$project/$revision"
}
data object Profile : NavScreen("profile")
data object ProfileByEmail : NavScreen("profile_by_email?email={email}")
}

class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -158,6 +160,28 @@ class MainActivity : ComponentActivity() {
profileViewModel = profileViewModel,
)
}
composable(
route = NavScreen.ProfileByEmail.route,
arguments = listOf(navArgument("email") { type = NavType.StringType }),
deepLinks = listOf(navDeepLink { uriPattern = "https://treeherder.mozilla.org/jobs?repo={repo}&author={email}" }),
) { backStackEntry ->
val profileViewModel: ProfileViewModel = koinViewModel()
val encodedEmail = backStackEntry.arguments?.getString("email")

LaunchedEffect(encodedEmail) {
encodedEmail?.let {
val email = URLDecoder.decode(it, "UTF-8")
profileViewModel.updateAuthorEmail(email)
profileViewModel.searchByAuthor()
}
}

profileViewModel.onInstallApk = ::installApk
ProfileScreen(
onNavigateUp = { localNavController.popBackStack() },
profileViewModel = profileViewModel,
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -209,6 +210,12 @@ fun ProfileScreen(
val errorMessage by profileViewModel.errorMessage.collectAsState()
val cacheState by profileViewModel.cacheState.collectAsState()

LaunchedEffect(Unit) {
if (authorEmail.isBlank()) {
profileViewModel.loadLastSearchedEmail()
}
}

val isDownloading = remember(pushes) {
pushes.any { push ->
push.jobs.any { job ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ class ProfileViewModel(

init {
logcat(LogPriority.DEBUG, TAG) { "Initializing ProfileViewModel" }
viewModelScope.launch {
_authorEmail.value = userDataRepository.lastSearchedEmailFlow.first()
logcat(LogPriority.DEBUG, TAG) { "Initial author email loaded: ${_authorEmail.value}" }
}
cacheManager.cacheState.onEach { state ->
if (state is CacheManagementState.IdleEmpty) {
val updatedPushes = _pushes.value.map {
Expand All @@ -78,6 +74,16 @@ class ProfileViewModel(
}.launchIn(viewModelScope)
}

fun loadLastSearchedEmail() {
viewModelScope.launch {
val lastEmail = userDataRepository.lastSearchedEmailFlow.first()
if (lastEmail.isNotBlank() && _authorEmail.value.isBlank()) {
_authorEmail.value = lastEmail
logcat(LogPriority.DEBUG, TAG) { "Initial author email loaded: ${_authorEmail.value}" }
}
}
}

fun updateAuthorEmail(email: String) {
logcat(LogPriority.DEBUG, TAG) { "Updating author email to: $email" }
_authorEmail.value = email
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
package org.mozilla.tryfox.ui.screens

import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.api.io.TempDir
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.whenever
import org.mozilla.tryfox.data.IFenixRepository
import org.mozilla.tryfox.data.UserDataRepository
import org.mozilla.tryfox.data.managers.FakeCacheManager
Expand All @@ -25,60 +20,48 @@ import java.io.File
@ExtendWith(MockitoExtension::class)
class ProfileViewModelTest {

@JvmField
@RegisterExtension
val mainCoroutineRule = MainCoroutineRule()

private lateinit var viewModel: ProfileViewModel
private lateinit var fakeCacheManager: FakeCacheManager
private lateinit var cacheManager: FakeCacheManager

@Mock
private lateinit var mockFenixRepository: IFenixRepository
private lateinit var fenixRepository: IFenixRepository

@Mock
private lateinit var mockUserDataRepository: UserDataRepository
private lateinit var userDataRepository: UserDataRepository

@TempDir
lateinit var tempCacheDir: File

@BeforeEach
fun setUp() = runTest {
fakeCacheManager = FakeCacheManager(tempCacheDir)

// Mock the behavior of userDataRepository
whenever(mockUserDataRepository.lastSearchedEmailFlow).thenReturn(flowOf("[email protected]"))
cacheManager = FakeCacheManager(tempCacheDir)

viewModel = ProfileViewModel(
fenixRepository = mockFenixRepository,
userDataRepository = mockUserDataRepository,
cacheManager = fakeCacheManager,
fenixRepository = fenixRepository,
userDataRepository = userDataRepository,
cacheManager = cacheManager,
)
}

@AfterEach
fun tearDown() {
fakeCacheManager.reset()
cacheManager.reset()
}

@Test
fun `init loads last searched email`() = runTest {
advanceUntilIdle()
assertEquals("[email protected]", viewModel.authorEmail.value)
}
fun `updateAuthorEmail should update the authorEmail state`() = runTest {
// Given
val viewModel = ProfileViewModel(fenixRepository, userDataRepository, cacheManager)
val newEmail = "[email protected]"

@Test
fun `searchByAuthor with blank email sets error`() = runTest {
viewModel.updateAuthorEmail("")
viewModel.searchByAuthor()
advanceUntilIdle()
assertNotNull(viewModel.errorMessage.value)
assertTrue(viewModel.pushes.value.isEmpty())
}
viewModel.authorEmail.test {
assertEquals("", awaitItem()) // Consume initial value

@Test
fun `clearAppCache calls cacheManager`() = runTest {
viewModel.clearAppCache()
advanceUntilIdle()
assertTrue(fakeCacheManager.clearCacheCalled)
// When
viewModel.updateAuthorEmail(newEmail)

// Then
assertEquals(newEmail, awaitItem())
}
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mockitoKotlin = "6.0.0"
logcat = "0.4"
espressoIntents = "3.7.0" # Added javax.inject version
koin = "4.1.1"
turbine = "1.1.0"
composeMaterial = "1.6.8"

[libraries]
Expand Down Expand Up @@ -66,6 +67,7 @@ logcat = { group = "com.squareup.logcat", name = "logcat", version.ref = "logcat
androidx-espresso-intents = { group = "androidx.test.espresso", name = "espresso-intents", version.ref = "espressoIntents" } # Added javax.inject library
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down