1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package social.grain
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.local.ProfileInvalidation
import social.grain.feature.profile.ProfileFeedSource
import social.grain.feature.profile.ProfileGalleryFeedScreen
import social.grain.feature.profile.ProfileTab
import social.grain.feature.profile.ProfileViewModel
import social.grain.support.Fixtures
import social.grain.support.Harness
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitText
import social.grain.support.awaitValue
import social.grain.support.json
import social.grain.ui.theme.GrainTheme
/**
* The profile's grid unrolled into cards.
*
* What matters is where it opens: the tile that was tapped should be the first
* card on screen, with nothing above it composed, rather than the top of the
* list with a scroll still to come.
*/
@RunWith(AndroidJUnit4::class)
class ProfileGalleryFeedScreenTest {
@get:Rule
val mainDispatcher = MainDispatcherRule()
@get:Rule
val composeRule = createComposeRule()
private lateinit var harness: Harness
private val titles = listOf("First", "Second", "Third", "Fourth", "Fifth")
private fun feed(vararg titles: String) = titles
.mapIndexed { i, title -> json(Fixtures.gallery(rkey = "g$i", title = title)) }
.joinToString(",", prefix = """{"items":[""", postfix = "]}")
@Before
fun setUp() {
harness = Harness()
harness.server
.on("social.grain.actor.getProfile", json(Fixtures.profileDetailed()))
.on("social.grain.feed.getFeed", feed(*titles.toTypedArray()))
.on("social.grain.story.getStories", """{"stories":[]}""")
.on("social.grain.actor.getFavorites", feed("Faved"))
.on("social.grain.story.getArchive", """{"stories":[]}""")
}
@After
fun tearDown() = harness.tearDown()
private fun viewModel(did: String? = "did:plc:alice") = ProfileViewModel(
client = harness.client,
auth = harness.auth,
profileInvalidation = ProfileInvalidation(),
labelDefsCache = harness.labelDefinitions,
savedStateHandle = SavedStateHandle(
if (did == null) emptyMap() else mapOf("did" to did),
),
)
private fun show(
model: ProfileViewModel,
uri: String,
source: ProfileFeedSource = ProfileFeedSource.GALLERIES,
onBack: () -> Unit = {},
) {
composeRule.setContent {
GrainTheme {
ProfileGalleryFeedScreen(
uri = uri,
source = source,
onBack = onBack,
onOpenProfile = {},
onOpenHashtag = {},
viewModel = model,
)
}
}
composeRule.waitForIdle()
}
@Test
fun theFeedOpensOnTheTappedGalleryNotTheTopOfTheList() {
val model = viewModel()
val galleries = model.state.awaitValue("the grid") { it.galleries.size == titles.size }
.galleries
show(model, uri = galleries[3].uri)
composeRule.awaitText("Fourth")
composeRule.onNodeWithText("Fourth").assertIsDisplayed()
// Above the fold means not composed at all in a lazy list — the reader
// landed on the fourth card, not scrolled past the first three.
composeRule.onAllNodesWithText("First").assertCountEquals(0)
}
@Test
fun theFeedIsTitledWithTheProfilesHandleAndGoesBack() {
var back = 0
val model = viewModel()
val first = model.state.awaitValue("the grid") { it.galleries.isNotEmpty() }
.galleries.first()
show(model, uri = first.uri, onBack = { back += 1 })
composeRule.awaitText("alice.grain.social")
composeRule.awaitText("First")
composeRule.onNodeWithContentDescription("Back").performClick()
assertEquals(1, back)
}
@Test
fun aFeedFromTheFavoritesTabListsTheFavorites() {
harness.server.on(
"social.grain.actor.getProfile",
json(Fixtures.profileDetailed(did = harness.did, handle = harness.handle)),
)
val model = viewModel(did = null)
model.state.awaitValue("the profile") { it.profile != null }
model.selectTab(ProfileTab.FAVORITES)
val faved = model.state.awaitValue("the favorites") { it.favoritesLoaded }
.favorites.single()
show(model, uri = faved.uri, source = ProfileFeedSource.FAVORITES)
composeRule.awaitText("Favorites")
composeRule.awaitText("Faved")
composeRule.onAllNodesWithText("First").assertCountEquals(0)
}
@Test
fun aTappedGalleryThatIsNoLongerInTheListOpensAtTheTop() {
val model = viewModel()
model.state.awaitValue("the grid") { it.galleries.isNotEmpty() }
show(model, uri = "at://did:plc:alice/social.grain.gallery/gone")
composeRule.awaitText("First")
}
}