[Unit test lib] Add `runGlanceAppWidgetUnitTest` similar to compose's `runComposeUiTest` that provides scoped functions such as `provideContent`, `onNode` etc. to be able to write unit tests.
* `onAllNodes` and corresponding assertions on collections will be in follow up.
* Similarly more filters coming up in follow up.
* In general I'd imagine most Glance usages to load data in `provideGlance`, but since technically it is possible to use a LaunchedEffect, so, uses `runTest` (StandardTestDispatcher) and added a awaitIdle function if they would like to wait for delays etc. from their launched effects.
* In compose it also supports passing effectContext for launchedEffects, but that's mostly for usage with animations which we don't have. So didn't include it, but would rather let us see how it goes without it.
Bug: b/201779038
Test: Ran 60 iterations of "i=0; while echo "iteration $i" && ./gradlew :glance:glance-appwidget-testing:testDebugUnitTest ; do i=$((i+1)); done"
Relnote: """Adds `runGlanceAppWidgetUnitTest` that provides scope to
call methods on `GlanceAppWidgetUnitTest` such as
`provideComposable` to provide a small isolated composable for test,
`onNode` to find a Glance composable element in the provided
content. This enables you to write unit tests for individual
composable functions in your appWidget to verify that given
certain inputs the function outputs the intended set of glance
composable elements.
"""
Change-Id: I2f682d9ee00def5d768bd41f44cc097fc049794b
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index eabfc57..b7907db 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -190,7 +190,9 @@
docs(project(":fragment:fragment-testing"))
docs(project(":glance:glance"))
docs(project(":glance:glance-appwidget"))
+ docs(project(":glance:glance-appwidget-testing"))
samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
+ samples(project(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
docs(project(":glance:glance-appwidget-preview"))
docs(project(":glance:glance-preview"))
docs(project(":glance:glance-testing"))
diff --git a/glance/glance-appwidget-testing/api/current.txt b/glance/glance-appwidget-testing/api/current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/api/res-current.txt b/glance/glance-appwidget-testing/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/res-current.txt
diff --git a/glance/glance-appwidget-testing/api/restricted_current.txt b/glance/glance-appwidget-testing/api/restricted_current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/restricted_current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/build.gradle b/glance/glance-appwidget-testing/build.gradle
new file mode 100644
index 0000000..8d5db0b
--- /dev/null
+++ b/glance/glance-appwidget-testing/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesTest)
+ api(project(":glance:glance-testing"))
+ api(project(":glance:glance-appwidget"))
+
+ testImplementation("androidx.core:core:1.7.0")
+ testImplementation("androidx.core:core-ktx:1.7.0")
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+
+ samples(projectOrArtifact(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
+}
+
+android {
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing"
+}
+
+androidx {
+ name = "androidx.glance:glance-appwidget-testing"
+ type = LibraryType.PUBLISHED_LIBRARY
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "This library provides APIs for developers to use for testing their appWidget specific Glance composables."
+}
diff --git a/glance/glance-appwidget-testing/samples/build.gradle b/glance/glance-appwidget-testing/samples/build.gradle
new file mode 100644
index 0000000..a5da7fa1
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation(project(":glance:glance"))
+ implementation(project(":glance:glance-testing"))
+ implementation(project(":glance:glance-appwidget-testing"))
+
+ implementation(libs.junit)
+ implementation(libs.testCore)
+ implementation("androidx.core:core:1.7.0")
+ implementation("androidx.core:core-ktx:1.7.0")
+}
+
+androidx {
+ name = "Glance AppWidget Testing Samples"
+ type = LibraryType.SAMPLES
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "Contains the sample code for testing the Glance AppWidget Composables"
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing.samples"
+}
diff --git a/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
new file mode 100644
index 0000000..28a29c2
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 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.glance.appwidget.testing.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.width
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import org.junit.Test
+
+@Sampled
+@Suppress("unused")
+fun isolatedGlanceComposableTestSamples() {
+ class TestSample {
+ @Test
+ fun statusContent_statusFalse_outputsPending() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Pending"))
+ }
+
+ @Test
+ fun statusContent_statusTrue_outputsFinished() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = true
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Finished"))
+ }
+
+ @Test
+ fun header_smallSize_showsShortHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 50.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp"))
+ }
+
+ @Test
+ fun header_largeSize_showsLongHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 150.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp (Last order)"))
+ }
+
+ @Composable
+ fun WidgetContent(status: Boolean) {
+ Column {
+ Header()
+ Spacer()
+ StatusRow(status)
+ }
+ }
+
+ @Composable
+ fun Header() {
+ val width = LocalSize.current.width
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = if (width > 50.dp) {
+ "MyApp (Last order)"
+ } else {
+ "MyApp"
+ },
+ modifier = GlanceModifier.semantics { testTag = "header-text" }
+ )
+ }
+ }
+
+ @Composable
+ fun StatusRow(status: Boolean) {
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = "Status",
+ )
+ Spacer(modifier = GlanceModifier.width(10.dp))
+ Text(
+ text = if (status) {
+ "Pending"
+ } else {
+ "Finished"
+ },
+ modifier = GlanceModifier.semantics { testTag = "status-text" }
+ )
+ }
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
new file mode 100644
index 0000000..c6282eb
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023 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.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.testing.GlanceNodeAssertionsProvider
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+
+/**
+ * Sets up the test environment and runs the given unit [test block][block]. Use the methods on
+ * [GlanceAppWidgetUnitTest] in the test to provide Glance composable content, find Glance elements
+ * and make assertions on them.
+ *
+ * Test your individual Glance composable functions in isolation to verify that your logic outputs
+ * right elements. For example: if input data is 'x', an image 'y' was
+ * outputted. In sample below, the test class has a separate test for the header and the status
+ * row.
+ *
+ * Tests can be run on JVM as these don't involve rendering the UI. If your logic depends on
+ * [Context] or other android APIs, tests can be run on Android unit testing frameworks such as
+ * [Robolectric](https://github.com/robolectric/robolectric).
+ *
+ * Note: Keeping a reference to the [GlanceAppWidgetUnitTest] outside of this function is an error.
+ *
+ * @sample androidx.glance.appwidget.testing.samples.isolatedGlanceComposableTestSamples
+ *
+ * @param timeout test time out; defaults to 10s
+ * @param block The test block that involves calling methods in [GlanceAppWidgetUnitTest]
+ */
+// This and backing environment is based on pattern followed by
+// "androidx.compose.ui.test.runComposeUiTest". Alternative of exposing testRule was explored, but
+// it wasn't necessary for this case. If developers wish, they may use this function to create their
+// own test rule.
+fun runGlanceAppWidgetUnitTest(
+ timeout: Duration = DEFAULT_TIMEOUT,
+ block: GlanceAppWidgetUnitTest.() -> Unit
+) = GlanceAppWidgetUnitTestEnvironment(timeout).runTest(block)
+
+/**
+ * Provides methods to enable you to test your logic of building Glance composable content in the
+ * [runGlanceAppWidgetUnitTest] scope.
+ *
+ * @see [runGlanceAppWidgetUnitTest]
+ */
+sealed interface GlanceAppWidgetUnitTest :
+ GlanceNodeAssertionsProvider<MappedNode, GlanceMappedNode> {
+ /**
+ * Sets the size of the appWidget to be assumed for the test. This corresponds to the
+ * `LocalSize.current` composition local. If you are accessing the local size, you must
+ * call this method to set the intended size for the test.
+ *
+ * Note: This should be called before calling [provideComposable].
+ * Default is `349.dp, 455.dp` that of a 5x4 widget in Pixel 4 portrait mode. See
+ * [GlanceAppWidgetUnitTestDefaults.size]
+ *
+ * 1. If your appWidget uses `sizeMode == Single`, you can set this to the `minWidth` and
+ * `minHeight` set in your appwidget info xml.
+ * 2. If your appWidget uses `sizeMode == Exact`, you can identify the sizes to test looking
+ * at the documentation on
+ * [Determine a size for your widget](https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size).
+ * and identifying landscape and portrait sizes that your widget may appear on.
+ * 3. If your appWidget uses `sizeMode == Responsive`, you can set this to one of the sizes from
+ * the list that you provide when specifying the sizeMode.
+ */
+ fun setAppWidgetSize(size: DpSize)
+
+ /**
+ * Sets the state to be used for the test if your composable under test accesses it via
+ * `currentState<*>()` or `LocalState.current`.
+ *
+ * Default state is `null`. Note: This should be called before calling [provideComposable],
+ * updates to the state after providing content has no effect. This matches the appWidget
+ * behavior where you need to call `update` on the widget for state changes to take effect.
+ *
+ * @param state the state to be used for testing the composable.
+ * @param T type of state used in your [GlanceStateDefinition] e.g. `Preferences` if your state
+ * definition is `GlanceStateDefinition<Preferences>`
+ */
+ fun <T> setState(state: T)
+
+ /**
+ * Sets the context to be used for the test.
+ *
+ * It is optional to call this method. However, you must set this if your composable needs
+ * access to `LocalContext`. You may need to use a Android unit test framework such as
+ * [Robolectric](https://github.com/robolectric/robolectric) to get the context.
+ *
+ * Note: This should be called before calling [provideComposable], updates to the state after
+ * providing content has no effect
+ */
+ fun setContext(context: Context)
+
+ /**
+ * Sets the Glance composable function to be tested. Each unit test should test a composable in
+ * isolation and assume specific state as input. Prefer keeping composables side-effects free.
+ * Perform any state changes needed for the test before calling [provideComposable] or
+ * [runGlanceAppWidgetUnitTest].
+ *
+ * @param composable the composable function under test
+ */
+ fun provideComposable(composable: @Composable () -> Unit)
+
+ /**
+ * Wait until all recompositions are calculated. For example if you have `LaunchedEffect` with
+ * delays in your composable.
+ */
+ fun awaitIdle()
+}
+
+/**
+ * Provides default values for various properties used in the Glance appWidget unit tests.
+ */
+object GlanceAppWidgetUnitTestDefaults {
+ /**
+ * [GlanceId] that can be assumed for state updates testing a Glance composable in isolation.
+ */
+ fun glanceId(): GlanceId = AppWidgetId(1)
+
+ /**
+ * Default size of the appWidget assumed in the unit tests. To override the size, use the
+ * [GlanceAppWidgetUnitTest.setAppWidgetSize] function.
+ *
+ * The default `349.dp, 455.dp` is that of a 5x4 widget in Pixel 4 portrait mode.
+ */
+ fun size(): DpSize = DpSize(height = 349.dp, width = 455.dp)
+
+ /**
+ * Default category of the appWidget assumed in the unit tests.
+ *
+ * The default is `WIDGET_CATEGORY_HOME_SCREEN`
+ */
+ fun hostCategory(): Int = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
new file mode 100644
index 0000000..674c8c5
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 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.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.Recomposer
+import androidx.compose.ui.unit.DpSize
+import androidx.glance.Applier
+import androidx.glance.LocalContext
+import androidx.glance.LocalGlanceId
+import androidx.glance.LocalSize
+import androidx.glance.LocalState
+import androidx.glance.appwidget.LocalAppWidgetOptions
+import androidx.glance.appwidget.RemoteViewsRoot
+import androidx.glance.session.globalSnapshotMonitor
+import androidx.glance.testing.GlanceNodeAssertion
+import androidx.glance.testing.GlanceNodeMatcher
+import androidx.glance.testing.TestContext
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+
+internal val DEFAULT_TIMEOUT = 10.seconds
+
+/**
+ * An implementation of [GlanceAppWidgetUnitTest] that provides APIs to run composition for
+ * appwidget-specific Glance composable content.
+ */
+internal class GlanceAppWidgetUnitTestEnvironment(
+ private val timeout: Duration
+) : GlanceAppWidgetUnitTest {
+ private var testContext = TestContext<MappedNode, GlanceMappedNode>()
+ private var testScope = TestScope()
+
+ // Data for composition locals
+ private var context: Context? = null
+ private val fakeGlanceID = GlanceAppWidgetUnitTestDefaults.glanceId()
+ private var size: DpSize = GlanceAppWidgetUnitTestDefaults.size()
+ private var state: Any? = null
+
+ private val root = RemoteViewsRoot(10)
+
+ private lateinit var recomposer: Recomposer
+ private lateinit var composition: Composition
+
+ fun runTest(block: GlanceAppWidgetUnitTest.() -> Unit) = testScope.runTest(timeout) {
+ var snapshotMonitor: Job? = null
+ try {
+ // GlobalSnapshotManager.ensureStarted() uses Dispatcher.Default, so using
+ // globalSnapshotMonitor instead to be able to use test dispatcher instead.
+ snapshotMonitor = launch { globalSnapshotMonitor() }
+ val applier = Applier(root)
+ recomposer = Recomposer(testScope.coroutineContext)
+ composition = Composition(applier, recomposer)
+ block()
+ } finally {
+ composition.dispose()
+ snapshotMonitor?.cancel()
+ recomposer.cancel()
+ recomposer.join()
+ }
+ }
+
+ // Among the appWidgetOptions available, size related options shouldn't generally be necessary
+ // for developers to look up - the LocalSize composition local should suffice. So, currently, we
+ // only initialize host category.
+ private val appWidgetOptions = Bundle().apply {
+ putInt(
+ AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
+ GlanceAppWidgetUnitTestDefaults.hostCategory()
+ )
+ }
+
+ override fun provideComposable(composable: @Composable () -> Unit) {
+ check(testContext.rootGlanceNode == null) {
+ "provideComposable can only be called once"
+ }
+
+ testScope.launch {
+ var compositionLocals = arrayOf(
+ LocalGlanceId provides fakeGlanceID,
+ LocalState provides state,
+ LocalAppWidgetOptions provides appWidgetOptions,
+ LocalSize provides size
+ )
+ context?.let {
+ compositionLocals = compositionLocals.plus(LocalContext provides it)
+ }
+
+ composition.setContent {
+ CompositionLocalProvider(
+ values = compositionLocals,
+ content = composable,
+ )
+ }
+
+ launch(currentCoroutineContext() + TestFrameClock()) {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+
+ launch {
+ recomposer.currentState.collect { curState ->
+ when (curState) {
+ Recomposer.State.Idle -> {
+ testContext.rootGlanceNode = GlanceMappedNode(
+ emittable = root.copy()
+ )
+ }
+
+ Recomposer.State.ShutDown -> {
+ cancel()
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ override fun awaitIdle() {
+ testScope.testScheduler.advanceUntilIdle()
+ }
+
+ override fun onNode(
+ matcher: GlanceNodeMatcher<MappedNode>
+ ): GlanceNodeAssertion<MappedNode, GlanceMappedNode> {
+ // Always let all the enqueued tasks finish before inspecting the tree.
+ testScope.testScheduler.runCurrent()
+ // Calling onNode resets the previously matched nodes and starts a new matching chain.
+ testContext.reset()
+ // Delegates matching to the next assertion.
+ return GlanceNodeAssertion(matcher, testContext)
+ }
+
+ override fun setAppWidgetSize(size: DpSize) {
+ check(testContext.rootGlanceNode == null) {
+ "setApWidgetSize should be called before calling provideComposable"
+ }
+ this.size = size
+ }
+
+ override fun <T> setState(state: T) {
+ check(testContext.rootGlanceNode == null) {
+ "setState should be called before calling provideComposable"
+ }
+ this.state = state
+ }
+
+ override fun setContext(context: Context) {
+ check(testContext.rootGlanceNode == null) {
+ "setContext should be called before calling provideComposable"
+ }
+ this.context = context
+ }
+
+ /**
+ * Test clock that sends all frames immediately.
+ */
+ // Same as TestUtils.TestFrameClock used in Glance unit tests.
+ private class TestFrameClock : MonotonicFrameClock {
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R) =
+ onFrame(System.currentTimeMillis())
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/AndroidManifest.xml b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..f125c7b
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright 2023 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.
+ -->
+
+<manifest>
+ <application/>
+</manifest>
\ No newline at end of file
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
new file mode 100644
index 0000000..514826d
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 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.glance.appwidget.testing.unit
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.layout.Column
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [33])
+/**
+ * Holds tests that use Robolectric for providing application resources and context.
+ */
+class GlanceAppWidgetUnitTestEnvironmentRobolectricTest {
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ }
+
+ @Test
+ fun runTest_localContextRead() = runGlanceAppWidgetUnitTest {
+ setContext(context)
+
+ provideComposable {
+ ComposableReadingLocalContext()
+ }
+
+ onNode(hasTestTag("test-tag"))
+ .assert(hasText("Test string: MyTest"))
+ }
+
+ @Composable
+ fun ComposableReadingLocalContext() {
+ val context = LocalContext.current
+
+ Column {
+ Text(
+ text = "Test string: ${context.getString(R.string.glance_test_string)}",
+ modifier = GlanceModifier.semantics { testTag = "test-tag" }
+ )
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
new file mode 100644
index 0000000..a1449d8
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2023 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.glance.appwidget.testing.unit
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.preferencesOf
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.currentState
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import kotlinx.coroutines.delay
+import org.junit.Test
+
+// In this test we aren't specifically testing anything bound to SDK, so we can run it without
+// android unit test runners such as Robolectric.
+class GlanceAppWidgetUnitTestEnvironmentTest {
+ @Test
+ fun runTest_localSizeRead() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 120.dp, height = 200.dp))
+
+ provideComposable {
+ ComposableReadingLocalSize()
+ }
+
+ onNode(hasText("120.0 dp x 200.0 dp")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingLocalSize() {
+ val size = LocalSize.current
+ Column {
+ Text(text = "${size.width.value} dp x ${size.height.value} dp")
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ )
+ }
+ }
+
+ @Test
+ fun runTest_currentStateRead() = runGlanceAppWidgetUnitTest {
+ setState(preferencesOf(toggleKey to true))
+
+ provideComposable {
+ ComposableReadingState()
+ }
+
+ onNode(hasText("isToggled")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingState() {
+ Column {
+ Text(text = "A text")
+ Spacer()
+ Text(text = getTitle(currentState<Preferences>()[toggleKey] == true))
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ modifier = GlanceModifier.semantics { testTag = "img" }
+ )
+ }
+ }
+
+ @Test
+ fun runTest_onNodeCalledMultipleTimes() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ Text(text = "abc")
+ Spacer()
+ Text(text = "xyz")
+ }
+
+ onNode(hasText("abc")).assertExists()
+ // test context reset and new filter matched onNode
+ onNode(hasText("xyz")).assertExists()
+ onNode(hasText("def")).assertDoesNotExist()
+ }
+
+ @Test
+ fun runTest_effect() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelay() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ awaitIdle() // Since the launched effect has a delay.
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelayWithoutAdvancing() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("initial"))
+ }
+}
+
+private val toggleKey = booleanPreferencesKey("title_toggled_key")
+private fun getTitle(toggled: Boolean) = if (toggled) "isToggled" else "notToggled"
diff --git a/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
new file mode 100644
index 0000000..49a3142
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2023 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.
+ -->
+
+<vector android:alpha="0.9" android:height="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
+</vector>
diff --git a/glance/glance-appwidget-testing/src/test/res/values/strings.xml b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
new file mode 100644
index 0000000..88a0850
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 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.
+ -->
+
+<resources>
+ <string name="glance_test_string">MyTest</string>
+</resources>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index f667413..a1d03ba 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -23,6 +23,8 @@
import android.util.Log
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.Composable
import androidx.glance.GlanceComposable
import androidx.glance.GlanceId
@@ -194,7 +196,8 @@
}
}
-internal data class AppWidgetId(val appWidgetId: Int) : GlanceId
+@RestrictTo(Scope.LIBRARY_GROUP)
+data class AppWidgetId(val appWidgetId: Int) : GlanceId
/** Update all App Widgets managed by the [GlanceAppWidget] class. */
suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
index 1428527..df0e178 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
@@ -16,6 +16,8 @@
package androidx.glance.appwidget
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.glance.Emittable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
@@ -24,7 +26,8 @@
* Root view, with a maximum depth. No default value is specified, as the exact value depends on
* specific circumstances.
*/
-internal class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
+@RestrictTo(Scope.LIBRARY_GROUP)
+ class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
override var modifier: GlanceModifier = GlanceModifier
override fun copy(): Emittable = RemoteViewsRoot(maxDepth).also {
it.modifier = modifier
diff --git a/glance/glance-testing/api/current.txt b/glance/glance-testing/api/current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/current.txt
+++ b/glance/glance-testing/api/current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/api/restricted_current.txt b/glance/glance-testing/api/restricted_current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/restricted_current.txt
+++ b/glance/glance-testing/api/restricted_current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
new file mode 100644
index 0000000..624b529
--- /dev/null
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 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.glance.testing
+
+/**
+ * Provides an entry point into testing exposing methods to find glance nodes
+ */
+// Equivalent to "androidx.compose.ui.test.SemanticsNodeInteractionsProvider" from compose.
+interface GlanceNodeAssertionsProvider<R, T : GlanceNode<R>> {
+ /**
+ * Finds a Glance node that matches the given condition.
+ *
+ * Any subsequent operation on its result will expect exactly one element found and will throw
+ * [AssertionError] if none or more than one element is found.
+ *
+ * @param matcher Matcher used for filtering
+ */
+ fun onNode(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertion<R, T>
+}
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
index 1399349..1ab76a3 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
@@ -23,6 +23,13 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class TestContext<R, T : GlanceNode<R>> {
+ /**
+ * To be called on every onNode to restart matching and clear cache.
+ */
+ fun reset() {
+ cachedMatchedNodes = emptyList()
+ }
+
var rootGlanceNode: T? = null
var cachedMatchedNodes: List<GlanceNode<R>> = emptyList()
}
diff --git a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
index cebf899..d5f6d8d 100644
--- a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
@@ -17,6 +17,7 @@
package androidx.glance.session
import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.snapshots.Snapshot
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
@@ -33,7 +34,7 @@
* state changes). These will be sent on Dispatchers.Default.
* This is based on [androidx.compose.ui.platform.GlobalSnapshotManager].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY_GROUP)
object GlobalSnapshotManager {
private val started = AtomicBoolean(false)
private val sent = AtomicBoolean(false)
@@ -59,7 +60,8 @@
/**
* Monitors global snapshot state writes and sends apply notifications.
*/
-internal suspend fun globalSnapshotMonitor() {
+@RestrictTo(Scope.LIBRARY_GROUP)
+suspend fun globalSnapshotMonitor() {
val channel = Channel<Unit>(1)
val sent = AtomicBoolean(false)
val observerHandle = Snapshot.registerGlobalWriteObserver {
diff --git a/settings.gradle b/settings.gradle
index a6dd45a..ae94270 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -767,7 +767,9 @@
includeProject(":glance:glance-appwidget", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:glance-appwidget-samples", "glance/glance-appwidget/samples", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing:glance-appwidget-testing-samples", "glance/glance-appwidget-testing/samples", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark-target", [BuildType.GLANCE])