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