package social.grain import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import social.grain.ui.components.ImageZoomHost import social.grain.ui.components.LocalImageZoom import social.grain.ui.components.ZoomablePhoto import social.grain.ui.theme.GrainTheme /** * Pinch-to-zoom, which is drawn by a host at the root of the app rather than in * place. * * A photo blown up inside the card is clipped twice over — by the pager and by * the lazy list — so the zoom is a copy in a layer above both. What that buys * is only visible in a gesture: the state has to end up on the shared host, and * it has to be given back when the fingers lift. */ @RunWith(AndroidJUnit4::class) class ZoomablePhotoTest { @get:Rule val composeRule = createComposeRule() @Test fun aPhotoOutsideAHostIsStillJustAPhoto() { composeRule.setContent { GrainTheme { ZoomablePhoto( model = "https://cdn.grain.test/full/0.jpg", contentDescription = "A tree", modifier = Modifier.size(200.dp), ) } } composeRule.onNodeWithContentDescription("A tree").assertExists() } @Test fun pinchingInsideAHostHandsTheZoomToTheHostAndGivesItBack() { lateinit var zoom: social.grain.ui.components.ImageZoomState composeRule.setContent { GrainTheme { ImageZoomHost(Modifier.fillMaxSize()) { zoom = LocalImageZoom.current!! ZoomablePhoto( model = "https://cdn.grain.test/full/0.jpg", contentDescription = "A tree", modifier = Modifier.size(200.dp), ) } } } composeRule.onNodeWithContentDescription("A tree").performTouchInput { down(0, Offset(width * 0.4f, height * 0.5f)) down(1, Offset(width * 0.6f, height * 0.5f)) moveTo(0, Offset(width * 0.1f, height * 0.5f)) moveTo(1, Offset(width * 0.9f, height * 0.5f)) } composeRule.waitForIdle() assertTrue("The fingers spread, so the photo grew", zoom.scale > 1f) composeRule.onNodeWithContentDescription("A tree").performTouchInput { up(0) up(1) } composeRule.waitUntil(10_000) { zoom.source == null } assertEquals(1f, zoom.scale, 0.001f) } @Test fun aPinchOnADisabledPhotoIsIgnored() { lateinit var zoom: social.grain.ui.components.ImageZoomState composeRule.setContent { GrainTheme { ImageZoomHost(Modifier.fillMaxSize()) { zoom = LocalImageZoom.current!! Box(Modifier.size(200.dp)) { ZoomablePhoto( model = "https://cdn.grain.test/full/0.jpg", contentDescription = "Covered", enabled = false, modifier = Modifier.fillMaxSize(), ) } } } } composeRule.onNodeWithContentDescription("Covered").performTouchInput { down(0, Offset(width * 0.4f, height * 0.5f)) down(1, Offset(width * 0.6f, height * 0.5f)) moveTo(0, Offset(width * 0.1f, height * 0.5f)) moveTo(1, Offset(width * 0.9f, height * 0.5f)) up(0) up(1) } composeRule.waitForIdle() assertEquals("A covered photo must not zoom past its label", 1f, zoom.scale, 0.001f) } }