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
package social.grain
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.feature.profile.CollapsingHeaderTabs
import androidx.compose.ui.unit.dp
import social.grain.ui.theme.GrainTheme
/**
* The profile header is measured outside the pager and reported at whatever
* height the collapse has left it, which is easy to get wrong in a way that
* silently drops everything below the first row — the display name, the handle
* and the bio all vanished that way once.
*/
@RunWith(AndroidJUnit4::class)
class ProfileHeaderCollapseTest {
@get:Rule val composeRule = createComposeRule()
private val lines = (1..6).map { "header line $it" }
private fun setContent(scrollableContent: Boolean = false) {
composeRule.setContent {
GrainTheme {
Surface(Modifier.fillMaxSize()) {
CollapsingHeaderTabs(
pagerState = rememberPagerState { 2 },
header = { Column { lines.forEach { Text(it) } } },
tabRow = { Text("Galleries") },
content = {
if (scrollableContent) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.testTag("grid"),
) {
items((1..60).toList()) {
Box(Modifier.fillMaxWidth().height(120.dp)) { Text("$it") }
}
}
} else {
Text("photos")
}
},
)
}
}
}
}
@Test
fun everyLineOfTheHeaderIsShown() {
setContent()
lines.forEach { composeRule.onNodeWithText(it).assertIsDisplayed() }
}
@Test
fun theTabBarAndTheGridSitBelowTheWholeHeader() {
setContent()
val lastLine = composeRule.onNodeWithText(lines.last()).fetchSemanticsNode()
val tabs = composeRule.onNodeWithText("Galleries").fetchSemanticsNode()
val grid = composeRule.onNodeWithText("photos").fetchSemanticsNode()
val headerBottom = lastLine.positionInRoot.y + lastLine.size.height
assert(tabs.positionInRoot.y >= headerBottom) {
"tab bar overlapped the header: ${tabs.positionInRoot.y} < $headerBottom"
}
assert(grid.positionInRoot.y >= tabs.positionInRoot.y + tabs.size.height) {
"grid overlapped the tab bar: ${grid.positionInRoot.y} < ${tabs.positionInRoot.y}"
}
}
@Test
fun scrollingTheGridTakesTheHeaderWithItAndBringsItBack() {
setContent(scrollableContent = true)
val restingTop = composeRule.onNodeWithText(lines.first()).fetchSemanticsNode().positionInRoot.y
composeRule.onNodeWithTag("grid").performTouchInput { swipeUp() }
composeRule.waitForIdle()
val collapsedTop = composeRule.onNodeWithText(lines.first()).fetchSemanticsNode().positionInRoot.y
assert(collapsedTop < restingTop) {
"header did not collapse: $collapsedTop is not above $restingTop"
}
// The tab bar rides with the header rather than crossing it, whatever
// the collapse has done.
val lastLine = composeRule.onNodeWithText(lines.last()).fetchSemanticsNode()
val tabs = composeRule.onNodeWithText("Galleries").fetchSemanticsNode()
assert(tabs.positionInRoot.y >= lastLine.positionInRoot.y + lastLine.size.height) {
"tab bar overlapped the header once collapsed"
}
}
}