blob: 09cdb862ed160154c9e6af07c8db9433c278e259 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.runtime
import android.view.Choreographer
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
@MediumTest
@RunWith(AndroidJUnit4::class)
class SuspendingEffectsTests : BaseComposeTest() {
@get:Rule
override val activityRule = makeTestActivityRule()
@Test
fun testLaunchedTask() {
var counter by mutableStateOf(0)
// Used as a signal that LaunchedTask will await
val ch = Channel<Unit>(Channel.CONFLATED)
compose {
LaunchedEffect(Unit) {
counter++
ch.receive()
counter++
ch.receive()
counter++
}
}.then {
assertEquals(1, counter)
ch.trySend(Unit)
}.then {
assertEquals(2, counter)
ch.trySend(Unit)
}.then {
assertEquals(3, counter)
}
}
@Test
fun testAwaitFrameFromLaunchedTask() {
var choreographerTime by mutableStateOf(Long.MIN_VALUE)
var awaitFrameTime by mutableStateOf(Long.MAX_VALUE)
compose {
LaunchedEffect(Unit) {
withFrameNanos {
awaitFrameTime = it
}
}
DisposableEffect(true) {
Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
choreographerTime = frameTimeNanos
}
onDispose { }
}
}.then {
assertNotEquals(choreographerTime, Long.MIN_VALUE, "Choreographer callback never ran")
assertNotEquals(awaitFrameTime, Long.MAX_VALUE, "awaitFrameNanos callback never ran")
assertEquals(
choreographerTime, awaitFrameTime,
"expected same values from choreographer post and awaitFrameNanos"
)
}
}
@Test
fun testAwaitFrameFromRememberCoroutineScope() {
var choreographerTime by mutableStateOf(Long.MIN_VALUE)
var awaitFrameTime by mutableStateOf(Long.MAX_VALUE)
compose {
val scope = rememberCoroutineScope()
DisposableEffect(true) {
scope.launch {
withFrameNanos {
awaitFrameTime = it
}
}
Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
choreographerTime = frameTimeNanos
}
onDispose { }
}
}.then {
assertNotEquals(choreographerTime, Long.MIN_VALUE, "Choreographer callback never ran")
assertNotEquals(awaitFrameTime, Long.MAX_VALUE, "awaitFrameNanos callback never ran")
assertEquals(
choreographerTime, awaitFrameTime,
"expected same values from choreographer post and awaitFrameNanos"
)
}
}
@Test
fun testRememberCoroutineScopeActiveWithComposition() {
lateinit var coroutineScope: CoroutineScope
compose {
coroutineScope = rememberCoroutineScope()
}.then {
assertTrue(coroutineScope.isActive, "coroutine scope was active before dispose")
}.done()
assertFalse(coroutineScope.isActive, "coroutine scope was inactive after dispose")
}
@Test
fun testRememberCoroutineScopeActiveAfterLeave() {
var coroutineScope: CoroutineScope? = null
var toggle by mutableStateOf(true)
compose {
if (toggle) {
coroutineScope = rememberCoroutineScope()
}
}.then {
assertTrue(
coroutineScope?.isActive == true,
"coroutine scope should be active after initial composition"
)
}.then {
toggle = false
}.then {
assertTrue(
coroutineScope?.isActive == false,
"coroutine scope should be cancelled after leaving composition"
)
}
}
@OptIn(InternalComposeApi::class)
@Test
fun testCoroutineScopesHaveCorrectFrameClock() {
var recomposerClock: MonotonicFrameClock? = null
var launchedTaskClock: MonotonicFrameClock? = null
var rememberCoroutineScopeFrameClock: MonotonicFrameClock? = null
compose {
recomposerClock = currentComposer.applyCoroutineContext[MonotonicFrameClock]
LaunchedEffect(Unit) {
launchedTaskClock = coroutineContext[MonotonicFrameClock]
}
val rememberedScope = rememberCoroutineScope()
SideEffect {
rememberCoroutineScopeFrameClock =
rememberedScope.coroutineContext[MonotonicFrameClock]
}
}.then {
assertNotNull(recomposerClock, "Recomposer frameClock")
assertSame(recomposerClock, launchedTaskClock, "LaunchedTask clock")
assertSame(
recomposerClock, rememberCoroutineScopeFrameClock,
"rememberCoroutineScope clock"
)
}
}
@Test
fun testLaunchedTaskRunsAfter() {
var onCommitRan = false
var launchRanAfter = false
compose {
// Confirms that these run "out of order" with respect to one another because
// the launch runs dispatched.
LaunchedEffect(Unit) {
launchRanAfter = onCommitRan
}
SideEffect {
onCommitRan = true
}
}.then {
assertTrue(launchRanAfter, "expected LaunchedTask to run after later onCommit")
}
}
/*
// Forced to disable test due to a bug in the Kotlin compiler
// which caused this function to fail to build due to invalid bytecode
// Build fails with AnalyzerException: Incompatible stack heights
@Test
fun testRememberCoroutineScopeDisallowsParentJob() {
var coroutineScope: CoroutineScope? = null
compose {
coroutineScope = rememberCoroutineScope { Job() }
}.then {
val scope = coroutineScope
assertNotNull(scope, "received non-null coroutine scope")
val job = scope.coroutineContext[Job]
assertNotNull(job, "scope context contains a job")
assertTrue(job.isCompleted, "job is complete")
var cause: Throwable? = null
job.invokeOnCompletion { cause = it }
assertTrue(cause is IllegalArgumentException,
"scope Job should be failed with IllegalArgumentException")
}
}
*/
}