Skip to content

Commit c514cc9

Browse files
Titouan Thibaudtitooan
authored andcommitted
Handle deeplink for user's try link
1 parent 21ad2f5 commit c514cc9

File tree

7 files changed

+82
-43
lines changed

7 files changed

+82
-43
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,11 @@ dependencies {
139139
testRuntimeOnly(libs.junit.jupiter.engine)
140140
testImplementation(libs.junit.jupiter.params)
141141
testRuntimeOnly(libs.junit.platform.launcher)
142+
testImplementation(libs.kotlinx.coroutines.test)
142143
testImplementation(libs.mockito.core)
143144
testImplementation(libs.mockito.kotlin)
144145
testImplementation(libs.mockito.junit.jupiter)
145-
testImplementation(libs.kotlinx.coroutines.test)
146+
testImplementation(libs.turbine)
146147
androidTestImplementation(libs.androidx.junit)
147148
androidTestImplementation(libs.androidx.espresso.core)
148149
androidTestImplementation(libs.androidx.espresso.intents)

app/src/androidTest/java/org/mozilla/tryfox/MainActivityDeeplinkTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,20 @@ class MainActivityDeeplinkTest {
7171
composeTestRule.waitForIdle()
7272
composeTestRule.onNodeWithText(revision).assertExists()
7373
}
74+
75+
@Test
76+
fun testDeeplink_withAuthorEmail_populatesProfileScreen() {
77+
val email = "[email protected]"
78+
val encodedEmail = "tthibaud%40mozilla.com"
79+
val deeplinkUri = Uri.parse("https://treeherder.mozilla.org/jobs?repo=try&author=$encodedEmail")
80+
val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java).apply {
81+
action = Intent.ACTION_VIEW
82+
data = deeplinkUri
83+
}
84+
85+
ActivityScenario.launch<MainActivity>(intent).use {
86+
composeTestRule.waitForIdle()
87+
composeTestRule.onNodeWithText(email).assertExists()
88+
}
89+
}
7490
}

app/src/main/java/org/mozilla/tryfox/MainActivity.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.mozilla.tryfox.ui.screens.ProfileViewModel
2828
import org.mozilla.tryfox.ui.screens.TryFoxMainScreen
2929
import org.mozilla.tryfox.ui.theme.TryFoxTheme
3030
import java.io.File
31+
import java.net.URLDecoder
3132

3233
sealed class NavScreen(val route: String) {
3334
data object Home : NavScreen("home")
@@ -36,6 +37,7 @@ sealed class NavScreen(val route: String) {
3637
fun createRoute(project: String, revision: String) = "treeherder_search/$project/$revision"
3738
}
3839
data object Profile : NavScreen("profile")
40+
data object ProfileByEmail : NavScreen("profile_by_email?email={email}")
3941
}
4042

4143
class MainActivity : ComponentActivity() {
@@ -158,6 +160,28 @@ class MainActivity : ComponentActivity() {
158160
profileViewModel = profileViewModel,
159161
)
160162
}
163+
composable(
164+
route = NavScreen.ProfileByEmail.route,
165+
arguments = listOf(navArgument("email") { type = NavType.StringType }),
166+
deepLinks = listOf(navDeepLink { uriPattern = "https://treeherder.mozilla.org/jobs?repo={repo}&author={email}" }),
167+
) { backStackEntry ->
168+
val profileViewModel: ProfileViewModel = koinViewModel()
169+
val encodedEmail = backStackEntry.arguments?.getString("email")
170+
171+
LaunchedEffect(encodedEmail) {
172+
encodedEmail?.let {
173+
val email = URLDecoder.decode(it, "UTF-8")
174+
profileViewModel.updateAuthorEmail(email)
175+
profileViewModel.searchByAuthor()
176+
}
177+
}
178+
179+
profileViewModel.onInstallApk = ::installApk
180+
ProfileScreen(
181+
onNavigateUp = { localNavController.popBackStack() },
182+
profileViewModel = profileViewModel,
183+
)
184+
}
161185
}
162186
}
163187
}

app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import androidx.compose.material3.TopAppBar
4343
import androidx.compose.material3.TopAppBarDefaults
4444
import androidx.compose.material3.rememberTooltipState
4545
import androidx.compose.runtime.Composable
46+
import androidx.compose.runtime.LaunchedEffect
4647
import androidx.compose.runtime.collectAsState
4748
import androidx.compose.runtime.getValue
4849
import androidx.compose.runtime.remember
@@ -209,6 +210,12 @@ fun ProfileScreen(
209210
val errorMessage by profileViewModel.errorMessage.collectAsState()
210211
val cacheState by profileViewModel.cacheState.collectAsState()
211212

213+
LaunchedEffect(Unit) {
214+
if (authorEmail.isBlank()) {
215+
profileViewModel.loadLastSearchedEmail()
216+
}
217+
}
218+
212219
val isDownloading = remember(pushes) {
213220
pushes.any { push ->
214221
push.jobs.any { job ->

app/src/main/java/org/mozilla/tryfox/ui/screens/ProfileViewModel.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ class ProfileViewModel(
5656

5757
init {
5858
logcat(LogPriority.DEBUG, TAG) { "Initializing ProfileViewModel" }
59-
viewModelScope.launch {
60-
_authorEmail.value = userDataRepository.lastSearchedEmailFlow.first()
61-
logcat(LogPriority.DEBUG, TAG) { "Initial author email loaded: ${_authorEmail.value}" }
62-
}
6359
cacheManager.cacheState.onEach { state ->
6460
if (state is CacheManagementState.IdleEmpty) {
6561
val updatedPushes = _pushes.value.map {
@@ -78,6 +74,16 @@ class ProfileViewModel(
7874
}.launchIn(viewModelScope)
7975
}
8076

77+
fun loadLastSearchedEmail() {
78+
viewModelScope.launch {
79+
val lastEmail = userDataRepository.lastSearchedEmailFlow.first()
80+
if (lastEmail.isNotBlank() && _authorEmail.value.isBlank()) {
81+
_authorEmail.value = lastEmail
82+
logcat(LogPriority.DEBUG, TAG) { "Initial author email loaded: ${_authorEmail.value}" }
83+
}
84+
}
85+
}
86+
8187
fun updateAuthorEmail(email: String) {
8288
logcat(LogPriority.DEBUG, TAG) { "Updating author email to: $email" }
8389
_authorEmail.value = email
Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
package org.mozilla.tryfox.ui.screens
22

3+
import app.cash.turbine.test
34
import kotlinx.coroutines.ExperimentalCoroutinesApi
4-
import kotlinx.coroutines.flow.flowOf
5-
import kotlinx.coroutines.test.advanceUntilIdle
65
import kotlinx.coroutines.test.runTest
76
import org.junit.jupiter.api.AfterEach
87
import org.junit.jupiter.api.Assertions.assertEquals
9-
import org.junit.jupiter.api.Assertions.assertNotNull
10-
import org.junit.jupiter.api.Assertions.assertTrue
118
import org.junit.jupiter.api.BeforeEach
129
import org.junit.jupiter.api.Test
1310
import org.junit.jupiter.api.extension.ExtendWith
14-
import org.junit.jupiter.api.extension.RegisterExtension
1511
import org.junit.jupiter.api.io.TempDir
1612
import org.mockito.Mock
1713
import org.mockito.junit.jupiter.MockitoExtension
18-
import org.mockito.kotlin.whenever
1914
import org.mozilla.tryfox.data.IFenixRepository
2015
import org.mozilla.tryfox.data.UserDataRepository
2116
import org.mozilla.tryfox.data.managers.FakeCacheManager
@@ -25,60 +20,48 @@ import java.io.File
2520
@ExtendWith(MockitoExtension::class)
2621
class ProfileViewModelTest {
2722

28-
@JvmField
29-
@RegisterExtension
30-
val mainCoroutineRule = MainCoroutineRule()
31-
3223
private lateinit var viewModel: ProfileViewModel
33-
private lateinit var fakeCacheManager: FakeCacheManager
24+
private lateinit var cacheManager: FakeCacheManager
3425

3526
@Mock
36-
private lateinit var mockFenixRepository: IFenixRepository
27+
private lateinit var fenixRepository: IFenixRepository
3728

3829
@Mock
39-
private lateinit var mockUserDataRepository: UserDataRepository
30+
private lateinit var userDataRepository: UserDataRepository
4031

4132
@TempDir
4233
lateinit var tempCacheDir: File
4334

4435
@BeforeEach
4536
fun setUp() = runTest {
46-
fakeCacheManager = FakeCacheManager(tempCacheDir)
47-
48-
// Mock the behavior of userDataRepository
49-
whenever(mockUserDataRepository.lastSearchedEmailFlow).thenReturn(flowOf("[email protected]"))
37+
cacheManager = FakeCacheManager(tempCacheDir)
5038

5139
viewModel = ProfileViewModel(
52-
fenixRepository = mockFenixRepository,
53-
userDataRepository = mockUserDataRepository,
54-
cacheManager = fakeCacheManager,
40+
fenixRepository = fenixRepository,
41+
userDataRepository = userDataRepository,
42+
cacheManager = cacheManager,
5543
)
5644
}
5745

5846
@AfterEach
5947
fun tearDown() {
60-
fakeCacheManager.reset()
48+
cacheManager.reset()
6149
}
6250

6351
@Test
64-
fun `init loads last searched email`() = runTest {
65-
advanceUntilIdle()
66-
assertEquals("[email protected]", viewModel.authorEmail.value)
67-
}
52+
fun `updateAuthorEmail should update the authorEmail state`() = runTest {
53+
// Given
54+
val viewModel = ProfileViewModel(fenixRepository, userDataRepository, cacheManager)
55+
val newEmail = "[email protected]"
6856

69-
@Test
70-
fun `searchByAuthor with blank email sets error`() = runTest {
71-
viewModel.updateAuthorEmail("")
72-
viewModel.searchByAuthor()
73-
advanceUntilIdle()
74-
assertNotNull(viewModel.errorMessage.value)
75-
assertTrue(viewModel.pushes.value.isEmpty())
76-
}
57+
viewModel.authorEmail.test {
58+
assertEquals("", awaitItem()) // Consume initial value
7759

78-
@Test
79-
fun `clearAppCache calls cacheManager`() = runTest {
80-
viewModel.clearAppCache()
81-
advanceUntilIdle()
82-
assertTrue(fakeCacheManager.clearCacheCalled)
60+
// When
61+
viewModel.updateAuthorEmail(newEmail)
62+
63+
// Then
64+
assertEquals(newEmail, awaitItem())
65+
}
8366
}
8467
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mockitoKotlin = "6.0.0"
2525
logcat = "0.4"
2626
espressoIntents = "3.7.0" # Added javax.inject version
2727
koin = "4.1.1"
28+
turbine = "1.1.0"
2829
composeMaterial = "1.6.8"
2930

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

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

0 commit comments

Comments
 (0)