diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle index 5d3827b3a88a1..9cf620316e15c 100644 --- a/compose/ui/ui-test/build.gradle +++ b/compose/ui/ui-test/build.gradle @@ -147,10 +147,17 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) { implementation(libs.kotlinTest) } - skikoMain { - dependsOn(commonMain) + skikoMain.dependencies { + implementation(project(":lifecycle:lifecycle-common")) + implementation(project(":lifecycle:lifecycle-runtime")) } + skikoTest.dependencies { + implementation(libs.kotlinTest) + implementation(project(":lifecycle:lifecycle-runtime-compose")) + } + + skikoMain.dependsOn(commonMain) desktopMain.dependsOn(skikoMain) jsNativeMain.dependsOn(skikoMain) jsMain.dependsOn(jsNativeMain) @@ -168,6 +175,15 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) { androidCommonTest.dependsOn(commonTest) androidTest.dependsOn(androidCommonTest) androidAndroidTest.dependsOn(androidCommonTest) + + desktopTest { + dependsOn(skikoTest) + dependencies { + implementation(libs.skikoCurrentOs) + } + } + nativeTest.dependsOn(skikoTest) + wasmJsTest.dependsOn(skikoTest) } } diff --git a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt index 3f9f58b5df041..33e17f282b0b2 100644 --- a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt +++ b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.test import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.InternalComposeUiApi @@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.InfiniteAnimationPolicy +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.scene.ComposeScene @@ -39,6 +41,9 @@ import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.cancellation.CancellationException @@ -318,11 +323,12 @@ class SkikoComposeUiTest @InternalTestApi constructor( } override fun setContent(composable: @Composable () -> Unit) { + val content = ProvideTestCompositionLocals(composable) if (isOnUiThread()) { - scene.setContent(content = composable) + scene.setContent(content = content) } else { runOnUiThread { - scene.setContent(content = composable) + scene.setContent(content = content) } // Only wait for idleness if not on the UI thread. If we are on the UI thread, the @@ -429,6 +435,19 @@ class SkikoComposeUiTest @InternalTestApi constructor( private inner class TestComposeSceneContext : ComposeSceneContext { override val platformContext = TestContext() } + + private val lifecycleOwner = TestLifecycleOwner() + + override fun sendLifecycleEvent(event: Lifecycle.Event) { + lifecycleOwner.lifecycle.handleLifecycleEvent(event) + } + + private fun ProvideTestCompositionLocals(composable: @Composable () -> Unit): @Composable () -> Unit = { + CompositionLocalProvider( + LocalLifecycleOwner provides lifecycleOwner, + content = composable + ) + } } @ExperimentalTestApi @@ -443,4 +462,9 @@ actual sealed interface ComposeUiTest : SemanticsNodeInteractionsProvider { actual fun registerIdlingResource(idlingResource: IdlingResource) actual fun unregisterIdlingResource(idlingResource: IdlingResource) actual fun setContent(composable: @Composable () -> Unit) + fun sendLifecycleEvent(event: Lifecycle.Event) } + +private class TestLifecycleOwner : LifecycleOwner { + override val lifecycle = LifecycleRegistry(this) +} \ No newline at end of file diff --git a/compose/ui/ui-test/src/skikoTest/kotlin/LifecycleInTestsTest.kt b/compose/ui/ui-test/src/skikoTest/kotlin/LifecycleInTestsTest.kt new file mode 100644 index 0000000000000..fcd8192229e63 --- /dev/null +++ b/compose/ui/ui-test/src/skikoTest/kotlin/LifecycleInTestsTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.ui.test + +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalTestApi::class) +class LifecycleInTestsTest { + @Test + fun lifecycleInComposeTest() = runComposeUiTest { + var onCreatedEffectCalled = false + var onResumeEffectCalled = false + + setContent { + LifecycleEventEffect(Lifecycle.Event.ON_CREATE) { + onCreatedEffectCalled = true + } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + onResumeEffectCalled = true + } + } + + assertFalse(onCreatedEffectCalled) + assertFalse(onResumeEffectCalled) + + sendLifecycleEvent(Lifecycle.Event.ON_START) + assertTrue(onCreatedEffectCalled) + assertFalse(onResumeEffectCalled) + + sendLifecycleEvent(Lifecycle.Event.ON_RESUME) + assertTrue(onCreatedEffectCalled) + assertTrue(onResumeEffectCalled) + } +} \ No newline at end of file