~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
113 lines · 4.7 KB Raw History
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"
        }
    }
}