1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.glance.appwidget.testing.unit
18 
19 import android.appwidget.AppWidgetProviderInfo
20 import android.content.Context
21 import androidx.compose.runtime.Composable
22 import androidx.compose.ui.unit.DpSize
23 import androidx.compose.ui.unit.dp
24 import androidx.glance.GlanceId
25 import androidx.glance.appwidget.AppWidgetId
26 import androidx.glance.state.GlanceStateDefinition
27 import androidx.glance.testing.GlanceNodeAssertionsProvider
28 import androidx.glance.testing.unit.GlanceMappedNode
29 import androidx.glance.testing.unit.MappedNode
30 import kotlin.time.Duration
31 
32 /**
33  * Sets up the test environment and runs the given unit [test block][block]. Use the methods on
34  * [GlanceAppWidgetUnitTest] in the test to provide Glance composable content, find Glance elements
35  * and make assertions on them.
36  *
37  * Test your individual Glance composable functions in isolation to verify that your logic outputs
38  * right elements. For example: if input data is 'x', an image 'y' was outputted. In sample below,
39  * the test class has a separate test for the header and the status row.
40  *
41  * Tests can be run on JVM as these don't involve rendering the UI. If your logic depends on
42  * [Context] or other android APIs, tests can be run on Android unit testing frameworks such as
43  * [Robolectric](https://github.com/robolectric/robolectric).
44  *
45  * Note: Keeping a reference to the [GlanceAppWidgetUnitTest] outside of this function is an error.
46  *
47  * @sample androidx.glance.appwidget.testing.samples.isolatedGlanceComposableTestSamples
48  * @param timeout test time out; defaults to 10s
49  * @param block The test block that involves calling methods in [GlanceAppWidgetUnitTest]
50  */
51 // This and backing environment is based on pattern followed by
52 // "androidx.compose.ui.test.runComposeUiTest". Alternative of exposing testRule was explored, but
53 // it wasn't necessary for this case. If developers wish, they may use this function to create their
54 // own test rule.
runGlanceAppWidgetUnitTestnull55 fun runGlanceAppWidgetUnitTest(
56     timeout: Duration = DEFAULT_TIMEOUT,
57     block: GlanceAppWidgetUnitTest.() -> Unit
58 ) = GlanceAppWidgetUnitTestEnvironment(timeout).runTest(block)
59 
60 /**
61  * Provides methods to enable you to test your logic of building Glance composable content in the
62  * [runGlanceAppWidgetUnitTest] scope.
63  *
64  * @see [runGlanceAppWidgetUnitTest]
65  */
66 sealed interface GlanceAppWidgetUnitTest :
67     GlanceNodeAssertionsProvider<MappedNode, GlanceMappedNode> {
68     /**
69      * Sets the size of the appWidget to be assumed for the test. This corresponds to the
70      * `LocalSize.current` composition local. If you are accessing the local size, you must call
71      * this method to set the intended size for the test.
72      *
73      * Note: This should be called before calling [provideComposable]. Default is `349.dp, 455.dp`
74      * that of a 5x4 widget in Pixel 4 portrait mode. See [GlanceAppWidgetUnitTestDefaults.size]
75      * 1. If your appWidget uses `sizeMode == Single`, you can set this to the `minWidth` and
76      *    `minHeight` set in your appwidget info xml.
77      * 2. If your appWidget uses `sizeMode == Exact`, you can identify the sizes to test looking at
78      *    the documentation on
79      *    [Determine a size for your widget](https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size).
80      *    and identifying landscape and portrait sizes that your widget may appear on.
81      * 3. If your appWidget uses `sizeMode == Responsive`, you can set this to one of the sizes from
82      *    the list that you provide when specifying the sizeMode.
83      */
84     fun setAppWidgetSize(size: DpSize)
85 
86     /**
87      * Sets the state to be used for the test if your composable under test accesses it via
88      * `currentState<*>()` or `LocalState.current`.
89      *
90      * Default state is `null`. Note: This should be called before calling [provideComposable],
91      * updates to the state after providing content has no effect. This matches the appWidget
92      * behavior where you need to call `update` on the widget for state changes to take effect.
93      *
94      * @param state the state to be used for testing the composable.
95      * @param T type of state used in your [GlanceStateDefinition] e.g. `Preferences` if your state
96      *   definition is `GlanceStateDefinition<Preferences>`
97      */
98     fun <T> setState(state: T)
99 
100     /**
101      * Sets the context to be used for the test.
102      *
103      * It is optional to call this method. However, you must set this if your composable needs
104      * access to `LocalContext`. You may need to use a Android unit test framework such as
105      * [Robolectric](https://github.com/robolectric/robolectric) to get the context.
106      *
107      * Note: This should be called before calling [provideComposable], updates to the state after
108      * providing content has no effect
109      */
110     fun setContext(context: Context)
111 
112     /**
113      * Sets the Glance composable function to be tested. Each unit test should test a composable in
114      * isolation and assume specific state as input. Prefer keeping composables side-effects free.
115      * Perform any state changes needed for the test before calling [provideComposable] or
116      * [runGlanceAppWidgetUnitTest].
117      *
118      * @param composable the composable function under test
119      */
120     fun provideComposable(composable: @Composable () -> Unit)
121 
122     /**
123      * Wait until all recompositions are calculated. For example if you have `LaunchedEffect` with
124      * delays in your composable.
125      */
126     fun awaitIdle()
127 }
128 
129 /** Provides default values for various properties used in the Glance appWidget unit tests. */
130 object GlanceAppWidgetUnitTestDefaults {
131     /**
132      * [GlanceId] that can be assumed for state updates testing a Glance composable in isolation.
133      */
glanceIdnull134     fun glanceId(): GlanceId = AppWidgetId(1)
135 
136     /**
137      * Default size of the appWidget assumed in the unit tests. To override the size, use the
138      * [GlanceAppWidgetUnitTest.setAppWidgetSize] function.
139      *
140      * The default `349.dp, 455.dp` is that of a 5x4 widget in Pixel 4 portrait mode.
141      */
142     fun size(): DpSize = DpSize(height = 349.dp, width = 455.dp)
143 
144     /**
145      * Default category of the appWidget assumed in the unit tests.
146      *
147      * The default is `WIDGET_CATEGORY_HOME_SCREEN`
148      */
149     fun hostCategory(): Int = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
150 }
151