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
package social.grain
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.api.XrpcClient
import social.grain.data.api.getFeed
import social.grain.data.auth.AuthManager
import social.grain.data.model.GrainGallery
import social.grain.ui.components.GalleryCard
import social.grain.ui.theme.GrainTheme
import java.util.concurrent.TimeUnit
/**
* Renders real galleries through the real card.
*
* Feed data is messier than any fixture: portrait and landscape mixed in one
* gallery, missing titles, null fav counts, single-photo posts. Composing live
* responses catches the layout assumptions a hand-written sample would satisfy
* by accident — an `aspectRatio` of zero crashes the whole feed, not one card.
*/
@RunWith(AndroidJUnit4::class)
class GalleryCardRenderTest {
@get:Rule
val composeRule = createComposeRule()
private val client = XrpcClient(
baseUrl = AuthManager.SERVER_URL,
http = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build(),
json = Json {
ignoreUnknownKeys = true
explicitNulls = false
coerceInputValues = true
},
)
private fun liveGalleries(limit: Int): List<GrainGallery> = runBlocking {
client.getFeed(feed = "recent", limit = limit).items.orEmpty()
}
@Test
fun rendersAFeedOfLiveGalleriesWithoutCrashing() {
val galleries = liveGalleries(20)
assertTrue("No galleries to render", galleries.isNotEmpty())
composeRule.setContent {
GrainTheme {
LazyColumn(Modifier.fillMaxSize()) {
items(galleries, key = { it.uri }) { GalleryCard(gallery = it) }
}
}
}
composeRule.waitForIdle()
// Every card draws a favorite control, so finding one proves the first
// card composed all the way through rather than bailing early.
val favoriteNodes = composeRule.onAllNodesWithContentDescription("Favorite")
.fetchSemanticsNodes()
val unfavoriteNodes = composeRule.onAllNodesWithContentDescription("Unfavorite")
.fetchSemanticsNodes()
assertTrue(
"No engagement row rendered — the card didn't compose",
favoriteNodes.isNotEmpty() || unfavoriteNodes.isNotEmpty(),
)
}
@Test
fun rendersTheAuthorHandleOnTheFirstCard() {
val gallery = liveGalleries(1).first()
composeRule.setContent {
GrainTheme { GalleryCard(gallery = gallery) }
}
composeRule.waitForIdle()
val name = gallery.creator.displayName?.takeIf { it.isNotBlank() }
?: gallery.creator.handle
composeRule.onNodeWithText(name).assertExists()
}
@Test
fun aGalleryWithNoPhotosStillRenders() {
// The appview can return a gallery whose photos are all deleted; the
// card must degrade to header-plus-caption rather than throw.
val stripped = liveGalleries(1).first().copy(items = emptyList())
composeRule.setContent {
GrainTheme { GalleryCard(gallery = stripped) }
}
composeRule.waitForIdle()
}
}