Support completable animations in Compose tests (#2051)

The change in #1987 to always use `withInfiniteAnimationFrameNanos` prevents non-infinite animations from completing in Compose tests. 

I added a simple check to use `withFrameNanos` or `withInfiniteAnimationFrameNanos` based on the number of iterations. This fixes tests that wait for animations to complete, see sample `WalkthroughAnimationTest`. And `InfiniteAnimationTest` still passes.

Let me know if I'm missing some other use case.
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt
index 16863f6..6403bca 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt
@@ -10,6 +10,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
 import com.airbnb.lottie.LottieComposition
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.NonCancellable
@@ -255,10 +256,22 @@
         }
     }
 
-    // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
-    // element that will cancel infinite transitions instead of preventing composition from ever going idle.
-    private suspend fun doFrame(iterations: Int): Boolean = withInfiniteAnimationFrameNanos { frameNanos ->
-        val composition = composition ?: return@withInfiniteAnimationFrameNanos true
+    private suspend fun doFrame(iterations: Int): Boolean {
+        return if (iterations == LottieConstants.IterateForever) {
+            // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
+            // element that will cancel infinite transitions instead of preventing composition from ever going idle.
+            withInfiniteAnimationFrameNanos { frameNanos ->
+                onFrame(iterations, frameNanos)
+            }
+        } else {
+            withFrameNanos { frameNanos ->
+                onFrame(iterations, frameNanos)
+            }
+        }
+    }
+
+    private fun onFrame(iterations: Int, frameNanos: Long): Boolean {
+        val composition = composition ?: return true
         val dNanos = if (lastFrameNanos == AnimationConstants.UnspecifiedTime) 0L else (frameNanos - lastFrameNanos)
         lastFrameNanos = frameNanos
 
@@ -279,7 +292,7 @@
             if (iteration + dIterations > iterations) {
                 progress = endProgress
                 iteration = iterations
-                return@withInfiniteAnimationFrameNanos false
+                return false
             }
             iteration += dIterations
             val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
@@ -289,7 +302,7 @@
             }
         }
 
-        true
+        return true
     }
 }
 
diff --git a/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt b/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt
new file mode 100644
index 0000000..ec62397
--- /dev/null
+++ b/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt
@@ -0,0 +1,46 @@
+package com.airbnb.lottie.samples
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.sample.compose.ComposeActivity
+import com.airbnb.lottie.sample.compose.R
+import org.junit.Rule
+import org.junit.Test
+
+class WalkthroughAnimationTest {
+
+    @get:Rule
+    val composeTestRule = createAndroidComposeRule(ComposeActivity::class.java)
+
+    @Test
+    fun testWalkthroughCompletes() {
+        val composition = LottieCompositionFactory.fromRawResSync(composeTestRule.activity, R.raw.walkthrough).value!!
+        var animationCompleted = true
+
+        composeTestRule.setContent {
+            val progress by animateLottieCompositionAsState(composition, iterations = 1)
+
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+            ) {
+                LottieAnimation(
+                    composition,
+                    progress,
+                )
+            }
+
+            if (progress == 1f) {
+                animationCompleted = true
+            }
+        }
+
+        composeTestRule.mainClock.advanceTimeUntil { animationCompleted }
+    }
+}
\ No newline at end of file