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.testing
18 
19 import androidx.annotation.RestrictTo
20 
21 /**
22  * Represents a collection of Glance nodes from the tree that can be asserted on.
23  *
24  * An instance of [GlanceNodeAssertionCollection] can be obtained from
25  * [GlanceNodeAssertionsProvider.onAllNodes] and equivalent methods.
26  */
27 // Equivalent to SemanticsNodeInteractionCollection in compose.
28 class GlanceNodeAssertionCollection<R, T : GlanceNode<R>>
29 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
30 constructor(
31     private val testContext: TestContext<R, T>,
32     private val selector: GlanceNodeSelector<R>
33 ) {
34     /**
35      * Asserts that this collection of nodes is equal to the given [expectedCount].
36      *
37      * @throws AssertionError if the size is not equal to [expectedCount]
38      */
assertCountEqualsnull39     fun assertCountEquals(expectedCount: Int): GlanceNodeAssertionCollection<R, T> {
40         val errorMessageOnFail = "Failed to assert count of nodes"
41 
42         val actualCount = testContext.findMatchingNodes(selector, errorMessageOnFail).size
43         if (actualCount != expectedCount) {
44             throw AssertionError(
45                 buildErrorMessageWithReason(
46                     errorMessageOnFail = errorMessageOnFail,
47                     reason =
48                         buildErrorReasonForCountMismatch(
49                             matcherDescription = selector.description,
50                             expectedCount = expectedCount,
51                             actualCount = actualCount
52                         )
53                 )
54             )
55         }
56         return this
57     }
58 
59     /**
60      * Asserts that all the nodes in this collection satisfy the given [matcher].
61      *
62      * Doesn't throw error if the collection is empty. Use [assertCountEquals] to assert on expected
63      * size of the collection.
64      *
65      * @param matcher Matcher that has to be satisfied by all the nodes in the collection.
66      * @throws AssertionError if the collection contains at least one element that does not satisfy
67      *   the given matcher.
68      */
assertAllnull69     fun assertAll(
70         matcher: GlanceNodeMatcher<R>,
71     ): GlanceNodeAssertionCollection<R, T> {
72         val errorMessageOnFail = "Failed to assertAll(${matcher.description})"
73 
74         val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
75         val violations = filteredNodes.filter { !matcher.matches(it) }
76         if (violations.isNotEmpty()) {
77             throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, violations))
78         }
79         return this
80     }
81 
82     /**
83      * Asserts that this collection contains at least one element that satisfies the given
84      * [matcher].
85      *
86      * @param matcher Matcher that has to be satisfied by at least one of the nodes in the
87      *   collection.
88      * @throws AssertionError if not at least one matching node was found.
89      */
assertAnynull90     fun assertAny(
91         matcher: GlanceNodeMatcher<R>,
92     ): GlanceNodeAssertionCollection<R, T> {
93         val errorMessageOnFail = "Failed to assertAny(${matcher.description})"
94         val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
95 
96         if (filteredNodes.isEmpty()) {
97             throw AssertionError(
98                 buildErrorMessageWithReason(
99                     errorMessageOnFail = errorMessageOnFail,
100                     reason = buildErrorReasonForAtLeastOneNodeExpected(selector.description)
101                 )
102             )
103         }
104 
105         if (!matcher.matchesAny(filteredNodes)) {
106             throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, filteredNodes))
107         }
108         return this
109     }
110 
111     /**
112      * Returns a [GlanceNodeAssertion] that can assert on the node at the given index of this
113      * collection.
114      *
115      * Any subsequent assertion on its result will throw error if index is out of bounds of the
116      * matching nodes found from previous operations.
117      */
getnull118     operator fun get(index: Int): GlanceNodeAssertion<R, T> {
119         return GlanceNodeAssertion(
120             testContext = testContext,
121             selector = selector.addIndexedSelector(index)
122         )
123     }
124 
125     /**
126      * Returns a new collection of nodes by filtering the given nodes using the provided [matcher].
127      */
filternull128     fun filter(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertionCollection<R, T> {
129         return GlanceNodeAssertionCollection(
130             testContext,
131             selector.addMatcherSelector(selectorName = "filter", matcher = matcher)
132         )
133     }
134 }
135