Skip to content

Commit

Permalink
Move the test for async placeholders to ZoomableImageTest
Browse files Browse the repository at this point in the history
Doing this because the issue wasn't limited to Coil.
  • Loading branch information
saket committed Apr 27, 2024
1 parent 894bcfc commit 81af516
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.dp
import app.cash.molecule.RecompositionMode
Expand All @@ -31,7 +29,6 @@ import assertk.assertions.isNotNull
import coil.Coil
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.compose.rememberAsyncImagePainter
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.imageLoader
Expand All @@ -44,7 +41,6 @@ import com.dropbox.dropshots.Dropshots
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
Expand All @@ -53,12 +49,10 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import leakcanary.LeakAssertions
import me.saket.telephoto.subsamplingimage.ImageBitmapOptions
import me.saket.telephoto.subsamplingimage.SubSamplingImageSource
import me.saket.telephoto.util.CiScreenshotValidator
import me.saket.telephoto.util.compositionLocalProviderReturnable
import me.saket.telephoto.util.prepareForScreenshotTest
import me.saket.telephoto.util.waitUntil
import me.saket.telephoto.zoomable.ZoomableImage
import me.saket.telephoto.zoomable.ZoomableImageSource
import me.saket.telephoto.zoomable.ZoomableImageSource.ResolveResult
import me.saket.telephoto.zoomable.ZoomableImageState
Expand Down Expand Up @@ -118,7 +112,6 @@ class CoilImageSourceTest {
override fun dispatch(request: RecordedRequest): MockResponse {
return when (request.path) {
"/placeholder_image.png" -> assetAsResponse("placeholder_image.png")
"/slow_placeholder_image.png" -> assetAsResponse("placeholder_image.png", delay = 1000.milliseconds)
"/full_image.png" -> assetAsResponse("full_image.png", delay = 300.milliseconds)
"/animated_image.gif" -> assetAsResponse("animated_image.gif", delay = 300.milliseconds)
"/single_frame_gif.gif" -> assetAsResponse("single_frame_gif.gif", delay = 300.milliseconds)
Expand Down Expand Up @@ -257,67 +250,6 @@ class CoilImageSourceTest {
}
}

@Test fun uses_updated_async_placeholder_size_when_available() = runTest {
val imageUrl = serverRule.server.url("/slow_placeholder_image.png")
lateinit var state: ZoomableImageState
var displayImage by mutableStateOf(false)
var placeholderLoaded = false

rule.setContent {
state = rememberZoomableImageState()

val placeholderPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.data(imageUrl)
.allowHardware(false) // Unsupported by Screenshot.capture()
.listener(
onSuccess = { _, _ ->
placeholderLoaded = true
}
)
.build()
)
val source = object : ZoomableImageSource {
@Composable
override fun resolve(canvasSize: Flow<Size>): ResolveResult {
val delegate = remember(displayImage) {
if (displayImage) {
ZoomableImageSource.SubSamplingDelegate(SubSamplingImageSource.asset("full_image.png"))
} else {
null
}
}

return ResolveResult(
delegate = delegate,
placeholder = placeholderPainter,
)
}
}

ZoomableImage(
modifier = Modifier
.fillMaxSize()
.testTag("image"),
image = source,
contentDescription = null,
state = state,
)
}

rule.waitUntil(5.seconds) { placeholderLoaded }
rule.waitForIdle()
rule.runOnIdle {
dropshots.assertSnapshot(rule.activity, name = "${testName.methodName}_placeholder")
}
displayImage = true
rule.waitForIdle()
rule.waitUntil(5.seconds) { state.isImageDisplayed }
rule.runOnIdle {
dropshots.assertSnapshot(rule.activity, name = "${testName.methodName}_full_image")
}
}

@Test fun reload_image_when_image_request_changes() = runTest {
var imageUrl by mutableStateOf(serverRule.server.url("placeholder_image.png"))

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.ColorPainter
Expand Down Expand Up @@ -78,6 +79,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest
import leakcanary.LeakAssertions
import me.saket.telephoto.subsamplingimage.SubSamplingImageSource
import me.saket.telephoto.util.CiScreenshotValidator
Expand All @@ -95,6 +97,7 @@ import org.junit.Test
import org.junit.rules.TestName
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.InputStream
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalFoundationApi::class)
Expand Down Expand Up @@ -1159,6 +1162,66 @@ class ZoomableImageTest {
}
}

@Test fun uses_updated_async_placeholder_size_when_available() = runTest {
lateinit var state: ZoomableImageState

val asyncPlaceholderPainter = PainterStub(initialSize = Size.Unspecified)
val imageSource = object : ZoomableImageSource {
@Composable
override fun resolve(canvasSize: Flow<Size>): ResolveResult {
return ResolveResult(
delegate = null,
placeholder = asyncPlaceholderPainter,
)
}
}

rule.setContent {
state = rememberZoomableImageState()
ZoomableImage(
modifier = Modifier.fillMaxSize(),
image = imageSource,
contentDescription = null,
state = state,
)
}

// Bug description: https://github.com/saket/telephoto/pull/84
// When the placeholder's intrinsic size is updated, the preview wasn't using the updated size.
// When using an AsyncImagePainter from Coil, this was causing the preview to permanently use
// Size.Unspecified, causing the placeholder to fill the view.
rule.waitForIdle()
asyncPlaceholderPainter.loadImage {
rule.activity.assets.open("fox_250.jpg")
}

rule.waitUntil(5.seconds) { asyncPlaceholderPainter.loaded }
rule.runOnIdle {
dropshots.assertSnapshot(rule.activity, name = "${testName.methodName}_placeholder")
}
}

private class PainterStub(private val initialSize: Size) : Painter() {
private var delegatePainter: Painter? by mutableStateOf(null)
private var loaded = false

override val intrinsicSize: Size
get() = delegatePainter?.intrinsicSize ?: initialSize

override fun DrawScope.onDraw() {
delegatePainter?.run {
draw(size)
}
}

fun loadImage(imageStream: () -> InputStream) {
delegatePainter = imageStream().use { stream ->
BitmapPainter(BitmapFactory.decodeStream(stream).asImageBitmap())
}
loaded = true
}
}

@Suppress("unused")
enum class LayoutSizeParam(val modifier: Modifier) {
FillMaxSize(Modifier.fillMaxSize()),
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package me.saket.telephoto.zoomable.internal

import androidx.annotation.RestrictTo
import androidx.compose.runtime.RememberObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down

0 comments on commit 81af516

Please sign in to comment.