GlanceAppWidgetUnitTest.kt

/*
 * 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
}