Skip to content

Commit 48fa368

Browse files
committed
merge: #50 GitHub-backed lists (PR)
2 parents 68ece5e + d384ff3 commit 48fa368

41 files changed

Lines changed: 3004 additions & 92 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎app/src/main/java/com/interlinedlist/android/navigation/InterlinedListNavHost.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.interlinedlist.android.navigation
22

3+
import android.content.Intent
4+
import android.net.Uri
35
import androidx.compose.foundation.layout.padding
46
import androidx.compose.material.icons.Icons
57
import androidx.compose.material.icons.automirrored.filled.List
@@ -352,13 +354,18 @@ private fun MainShell(
352354
onOpenConnections = { tabNav.navigate(Routes.LIST_CONNECTIONS) },
353355
onOpenSharedWithMe = { tabNav.navigate(Routes.LISTS_SHARED_WITH_ME) },
354356
onOpenFolders = { tabNav.navigate(Routes.LIST_FOLDERS) },
357+
// A GitHub-backed list needs GitHub linked with the Issues
358+
// scope; linking is an OAuth flow this app does not drive, so
359+
// the picker routes to the existing connected-accounts screen.
360+
onOpenConnectedAccounts = { tabNav.navigate(Routes.INTEGRATIONS_ACCOUNTS) },
355361
)
356362
}
357363
composable(
358364
Routes.LIST_DETAIL,
359365
arguments = listOf(navArgument("listId") { type = NavType.StringType }),
360366
) { entry ->
361367
val listId = entry.arguments?.getString("listId").orEmpty()
368+
val context = LocalContext.current
362369
ListDetailRoute(
363370
onBack = { tabNav.popBackStack() },
364371
onListDeleted = { tabNav.popBackStack() },
@@ -367,6 +374,13 @@ private fun MainShell(
367374
onOpenShare = { tabNav.navigate(Routes.listShare(listId)) },
368375
// Breadcrumb hops and newly created child lists open a detail route.
369376
onOpenList = { id -> tabNav.navigate(Routes.listDetail(id)) },
377+
// The repository link under a GitHub-backed list's title opens
378+
// that repo's issues page in the browser, as the web app does.
379+
onOpenRepo = { url ->
380+
runCatching {
381+
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
382+
}
383+
},
370384
)
371385
}
372386
composable(
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.interlinedlist.android.feature.lists.ui.detail
2+
3+
import androidx.compose.ui.test.assertIsDisplayed
4+
import androidx.compose.ui.test.junit4.createComposeRule
5+
import androidx.compose.ui.test.onNodeWithTag
6+
import androidx.compose.ui.test.onNodeWithText
7+
import androidx.compose.ui.test.performClick
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import com.google.common.truth.Truth.assertThat
10+
import com.interlinedlist.android.core.designsystem.theme.InterlinedListTheme
11+
import com.interlinedlist.android.feature.lists.domain.GithubRepoLink
12+
import com.interlinedlist.android.feature.lists.domain.ListSchema
13+
import com.interlinedlist.android.feature.lists.domain.ListSource
14+
import com.interlinedlist.android.feature.lists.domain.ListSummary
15+
import org.junit.Rule
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
19+
/**
20+
* The repository link under a GitHub-backed list's title, and its **Private repo**
21+
* tag — which describes the repository's visibility on GitHub, never the list's.
22+
*/
23+
@RunWith(AndroidJUnit4::class)
24+
class GithubRepoLinkRowTest {
25+
26+
@get:Rule
27+
val composeRule = createComposeRule()
28+
29+
private fun setRow(isPrivate: Boolean?, onOpenRepo: (String) -> Unit = {}) {
30+
composeRule.setContent {
31+
InterlinedListTheme {
32+
GithubRepoLinkRow(
33+
repo = "octocat/Hello-World",
34+
githubRepoPrivate = isPrivate,
35+
onOpenRepo = onOpenRepo,
36+
)
37+
}
38+
}
39+
}
40+
41+
@Test
42+
fun linksTheRepositoryIssuesPage() {
43+
var opened: String? = null
44+
setRow(isPrivate = false) { opened = it }
45+
46+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.LINK).assertIsDisplayed()
47+
composeRule.onNodeWithText("octocat/Hello-World issues").assertIsDisplayed()
48+
49+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.LINK).performClick()
50+
assertThat(opened).isEqualTo("https://github.com/octocat/Hello-World/issues")
51+
}
52+
53+
@Test
54+
fun showsThePrivateRepoTag_whenTheRepositoryIsPrivate() {
55+
setRow(isPrivate = true)
56+
57+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_TAG).assertIsDisplayed()
58+
composeRule.onNodeWithText(GithubRepoLink.PRIVATE_TAG).assertIsDisplayed()
59+
}
60+
61+
@Test
62+
fun theTagExplainsTheRepositoryIsPrivate_notTheList() {
63+
setRow(isPrivate = true)
64+
65+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_TAG).performClick()
66+
67+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_EXPLANATION).assertIsDisplayed()
68+
// The copy names the repository, and says the list's visibility is separate.
69+
val explanation = GithubRepoLink.PRIVATE_TAG_EXPLANATION
70+
assertThat(explanation).contains("This repository is private on GitHub")
71+
assertThat(explanation).contains("separate from who can see this list")
72+
composeRule.onNodeWithText(explanation).assertIsDisplayed()
73+
}
74+
75+
@Test
76+
fun showsNoTag_whenTheRepositoryIsPublic() {
77+
setRow(isPrivate = false)
78+
79+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_TAG).assertDoesNotExist()
80+
}
81+
82+
@Test
83+
fun showsNoTag_whenTheRepositoryVisibilityIsNotYetKnown() {
84+
// A list that has not synced since the tag existed: unknown is never
85+
// presented as public, and it is never presented as private either.
86+
setRow(isPrivate = null)
87+
88+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.LINK).assertIsDisplayed()
89+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_TAG).assertDoesNotExist()
90+
}
91+
92+
@Test
93+
fun rendersUnderTheTitleOfAGithubBackedListOnly() {
94+
val githubList = ListSummary(
95+
id = "L1",
96+
title = "Repo issues",
97+
description = null,
98+
itemCount = 0,
99+
folderId = null,
100+
isPublic = false,
101+
updatedAt = null,
102+
parentId = null,
103+
source = ListSource.GITHUB,
104+
githubRepo = "octocat/Hello-World",
105+
githubRepoPrivate = true,
106+
)
107+
composeRule.setContent {
108+
InterlinedListTheme {
109+
ListDetailScreen(
110+
state = ListDetailUiState(
111+
summary = githubList,
112+
schema = ListSchema.EMPTY,
113+
isLoading = false,
114+
),
115+
onBack = {},
116+
onAddRow = {},
117+
onEditRow = {},
118+
onDeleteRow = {},
119+
onDeleteList = {},
120+
)
121+
}
122+
}
123+
124+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.ROW).assertIsDisplayed()
125+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.PRIVATE_TAG).assertIsDisplayed()
126+
// Refresh from GitHub is offered here and nowhere else.
127+
composeRule.onNodeWithTag(ListDetailTestTags.REFRESH).assertIsDisplayed()
128+
}
129+
130+
@Test
131+
fun aLocalListShowsNoRepoLinkAndNoRefresh() {
132+
composeRule.setContent {
133+
InterlinedListTheme {
134+
ListDetailScreen(
135+
state = ListDetailUiState(
136+
summary = ListSummary("L1", "Reading", null, 0, null, false, null),
137+
schema = ListSchema.EMPTY,
138+
isLoading = false,
139+
),
140+
onBack = {},
141+
onAddRow = {},
142+
onEditRow = {},
143+
onDeleteRow = {},
144+
onDeleteList = {},
145+
)
146+
}
147+
}
148+
149+
composeRule.onNodeWithTag(GithubRepoLinkTestTags.ROW).assertDoesNotExist()
150+
composeRule.onNodeWithTag(ListDetailTestTags.REFRESH).assertDoesNotExist()
151+
}
152+
}

‎feature/lists/src/androidTest/kotlin/com/interlinedlist/android/feature/lists/ui/list/ListsScreenTest.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ListsScreenTest {
3434
onOpenConnections = {},
3535
onSearchQueryChange = {},
3636
onLoadMore = {},
37-
onCreateList = {},
37+
onNewList = {},
3838
)
3939
}
4040
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.interlinedlist.android.feature.lists.ui.list
2+
3+
import androidx.compose.ui.test.assertIsDisplayed
4+
import androidx.compose.ui.test.assertIsNotEnabled
5+
import androidx.compose.ui.test.junit4.createComposeRule
6+
import androidx.compose.ui.test.onNodeWithTag
7+
import androidx.compose.ui.test.performClick
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import com.google.common.truth.Truth.assertThat
10+
import com.interlinedlist.android.core.designsystem.theme.InterlinedListTheme
11+
import com.interlinedlist.android.feature.lists.domain.GithubRepo
12+
import com.interlinedlist.android.feature.lists.ui.github.GithubLinkProblem
13+
import org.junit.Rule
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
17+
/**
18+
* The "New list" sheet, in particular the GitHub half: picking a repository, and
19+
* what a user without GitHub linked is shown instead of a broken picker.
20+
*/
21+
@RunWith(AndroidJUnit4::class)
22+
class NewListSheetTest {
23+
24+
@get:Rule
25+
val composeRule = createComposeRule()
26+
27+
private val helloWorld = GithubRepo("octocat", "Hello-World")
28+
private val secretPlans = GithubRepo("acme", "secret-plans", isPrivate = true)
29+
30+
private fun setSheet(
31+
state: NewListUiState,
32+
onSelectRepo: (GithubRepo) -> Unit = {},
33+
onOpenConnectedAccounts: () -> Unit = {},
34+
) {
35+
composeRule.setContent {
36+
InterlinedListTheme {
37+
NewListSheet(
38+
state = state,
39+
onSelectKind = {},
40+
onTitleChange = {},
41+
onPublicChange = {},
42+
onRepoQueryChange = {},
43+
onSelectRepo = onSelectRepo,
44+
onSelectOrg = {},
45+
onCreate = {},
46+
onCancel = {},
47+
onOpenConnectedAccounts = onOpenConnectedAccounts,
48+
)
49+
}
50+
}
51+
}
52+
53+
@Test
54+
fun listsRepositories_andReportsTheOneThatIsPicked() {
55+
var picked: GithubRepo? = null
56+
setSheet(
57+
NewListUiState(
58+
kind = NewListKind.GITHUB,
59+
repos = listOf(helloWorld, secretPlans),
60+
),
61+
onSelectRepo = { picked = it },
62+
)
63+
64+
composeRule.onNodeWithTag(NewListTestTags.repo("octocat/Hello-World")).assertIsDisplayed()
65+
composeRule.onNodeWithTag(NewListTestTags.repo("acme/secret-plans")).performClick()
66+
67+
assertThat(picked).isEqualTo(secretPlans)
68+
}
69+
70+
@Test
71+
fun explainsAnUnlinkedAccount_andRoutesToConnectedAccounts() {
72+
var routed = false
73+
setSheet(
74+
NewListUiState(kind = NewListKind.GITHUB, linkProblem = GithubLinkProblem.NOT_LINKED),
75+
onOpenConnectedAccounts = { routed = true },
76+
)
77+
78+
composeRule.onNodeWithTag(NewListTestTags.LINK_PROBLEM).assertIsDisplayed()
79+
// No picker at all, rather than an empty one.
80+
composeRule.onNodeWithTag(NewListTestTags.REPO_SEARCH).assertDoesNotExist()
81+
composeRule.onNodeWithTag(NewListTestTags.CREATE).assertIsNotEnabled()
82+
83+
composeRule.onNodeWithTag(NewListTestTags.LINK_ACTION).performClick()
84+
assertThat(routed).isTrue()
85+
}
86+
87+
@Test
88+
fun explainsAnEmptyRepoList_asAnOrgApprovalProblem() {
89+
setSheet(NewListUiState(kind = NewListKind.GITHUB, repos = emptyList()))
90+
91+
composeRule.onNodeWithTag(NewListTestTags.REPO_EMPTY).assertIsDisplayed()
92+
}
93+
94+
@Test
95+
fun theTitleFieldWaitsForARepositoryToBeChosen() {
96+
setSheet(NewListUiState(kind = NewListKind.GITHUB, repos = listOf(helloWorld)))
97+
98+
// Nothing to title until there is a repository behind the list.
99+
composeRule.onNodeWithTag(NewListTestTags.TITLE).assertDoesNotExist()
100+
composeRule.onNodeWithTag(NewListTestTags.CREATE).assertIsNotEnabled()
101+
}
102+
103+
@Test
104+
fun theTitleFieldAppearsOnceARepositoryIsChosen() {
105+
setSheet(
106+
NewListUiState(
107+
kind = NewListKind.GITHUB,
108+
repos = listOf(helloWorld),
109+
selectedRepo = helloWorld,
110+
title = "Hello-World",
111+
),
112+
)
113+
114+
composeRule.onNodeWithTag(NewListTestTags.TITLE).assertIsDisplayed()
115+
composeRule.onNodeWithTag(NewListTestTags.VISIBILITY).assertIsDisplayed()
116+
}
117+
118+
@Test
119+
fun aLocalListNeedsNoRepositoryPicker() {
120+
setSheet(NewListUiState(kind = NewListKind.LOCAL, title = "Books"))
121+
122+
composeRule.onNodeWithTag(NewListTestTags.TITLE).assertIsDisplayed()
123+
composeRule.onNodeWithTag(NewListTestTags.REPO_SEARCH).assertDoesNotExist()
124+
composeRule.onNodeWithTag(NewListTestTags.LINK_PROBLEM).assertDoesNotExist()
125+
}
126+
}

0 commit comments

Comments
 (0)