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 package androidx.glance.testing
17 
18 import androidx.annotation.RestrictTo
19 
20 /**
21  * A context object that holds glance node tree being inspected as well as any state cached across
22  * the chain of assertions.
23  */
24 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
25 class TestContext<R, T : GlanceNode<R>> {
26     // e.g. RemoteViewsRoot
27     var rootGlanceNode: T? = null
28     private var allNodes: List<GlanceNode<R>> = emptyList()
29 
30     /**
31      * Returns all nodes in single flat list (either from cache or by traversing the hierarchy from
32      * root glance node).
33      */
getAllNodesnull34     private fun getAllNodes(): List<GlanceNode<R>> {
35         val rootGlanceNode = checkNotNull(rootGlanceNode) { "No root GlanceNode found." }
36         if (this.allNodes.isEmpty()) {
37             val allNodes = mutableListOf<GlanceNode<R>>()
38 
39             fun collectAllNodesRecursive(currentNode: GlanceNode<R>) {
40                 allNodes.add(currentNode)
41                 val children = currentNode.children()
42                 for (index in children.indices) {
43                     collectAllNodesRecursive(children[index])
44                 }
45             }
46 
47             collectAllNodesRecursive(rootGlanceNode)
48             this.allNodes = allNodes.toList()
49         }
50 
51         return this.allNodes
52     }
53 
54     /**
55      * Finds nodes matching the given selector from the list of all nodes in the hierarchy.
56      *
57      * @throws AssertionError if provided selector results in an error due to no match.
58      */
findMatchingNodesnull59     fun findMatchingNodes(
60         selector: GlanceNodeSelector<R>,
61         errorMessageOnFail: String
62     ): List<GlanceNode<R>> {
63         val allNodes = getAllNodes()
64         val selectionResult = selector.map(allNodes)
65 
66         if (selectionResult.errorMessageOnNoMatch != null) {
67             throw AssertionError(
68                 buildErrorMessageWithReason(
69                     errorMessageOnFail = errorMessageOnFail,
70                     reason = selectionResult.errorMessageOnNoMatch
71                 )
72             )
73         }
74 
75         return selectionResult.selectedNodes
76     }
77 
78     /**
79      * Returns true if root has glance nodes after composition to be able to perform assertions on.
80      *
81      * Can be false if either composable function produced no glance elements or composable function
82      * was not provided..
83      */
hasNodesnull84     fun hasNodes(): Boolean {
85         return rootGlanceNode?.children()?.isNotEmpty() ?: false
86     }
87 }
88