1 /*
2  * Copyright 2020 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.compose.ui.test
18 
19 import androidx.compose.ui.semantics.SemanticsActions
20 import androidx.compose.ui.semantics.SemanticsActions.OnImeAction
21 import androidx.compose.ui.semantics.SemanticsNode
22 import androidx.compose.ui.semantics.SemanticsProperties
23 import androidx.compose.ui.text.AnnotatedString
24 import androidx.compose.ui.text.TextRange
25 import androidx.compose.ui.text.input.ImeAction
26 
27 /** Clears the text in this node in similar way to IME. */
SemanticsNodeInteractionnull28 fun SemanticsNodeInteraction.performTextClearance() {
29     performTextReplacement("")
30 }
31 
32 /**
33  * Sends the given text to this node in similar way to IME.
34  *
35  * @param text Text to send.
36  */
SemanticsNodeInteractionnull37 fun SemanticsNodeInteraction.performTextInput(text: String) {
38     tryPerformAccessibilityChecks()
39     getNodeAndFocus()
40     performSemanticsAction(SemanticsActions.InsertTextAtCursor) { it(AnnotatedString(text)) }
41 }
42 
43 /**
44  * Sends the given selection to this node in similar way to IME.
45  *
46  * @param selection the selection to send
47  */
48 // Maintained for binary compatibility.
49 @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
SemanticsNodeInteractionnull50 fun SemanticsNodeInteraction.performTextInputSelection(selection: TextRange) {
51     performTextInputSelection(selection, relativeToOriginalText = true)
52 }
53 
54 /**
55  * Sends the given selection to this node in similar way to IME.
56  *
57  * @param selection the selection to send
58  * @param relativeToOriginalText `true` if the selection is relative to the untransformed, original
59  *   text. `false` if it is relative to the visual text following any transformations.
60  */
SemanticsNodeInteractionnull61 fun SemanticsNodeInteraction.performTextInputSelection(
62     selection: TextRange,
63     relativeToOriginalText: Boolean = true,
64 ) {
65     getNodeAndFocus(
66         errorOnFail = "Failed to perform text input selection.",
67         requireEditable = false,
68     )
69     performSemanticsAction(SemanticsActions.SetSelection) {
70         it(selection.min, selection.max, relativeToOriginalText)
71     }
72 }
73 
74 /**
75  * Replaces existing text with the given text in this node in similar way to IME.
76  *
77  * This does not reflect text selection. All the text gets cleared out and new inserted.
78  *
79  * @param text Text to send.
80  */
SemanticsNodeInteractionnull81 fun SemanticsNodeInteraction.performTextReplacement(text: String) {
82     getNodeAndFocus()
83     performSemanticsAction(SemanticsActions.SetText) { it(AnnotatedString(text)) }
84 }
85 
86 /**
87  * Sends to this node the IME action associated with it in a similar way to the IME.
88  *
89  * The node needs to define its IME action in semantics via
90  * [SemanticsPropertyReceiver.onImeAction][androidx.compose.ui.semantics.onImeAction].
91  *
92  * @throws AssertionError if the node does not support input or does not define IME action.
93  * @throws IllegalStateException if the node did is not an editor or would not be able to establish
94  *   an input connection (e.g. does not define [ImeAction][SemanticsProperties.ImeAction] or
95  *   [OnImeAction] or is not focused).
96  */
SemanticsNodeInteractionnull97 fun SemanticsNodeInteraction.performImeAction() {
98     val errorOnFail = "Failed to perform IME action."
99     assert(hasPerformImeAction()) { errorOnFail }
100     assert(!hasImeAction(ImeAction.Default)) { errorOnFail }
101     tryPerformAccessibilityChecks()
102     val node = getNodeAndFocus(errorOnFail, requireEditable = false)
103 
104     wrapAssertionErrorsWithNodeInfo(selector, node) {
105         performSemanticsAction(OnImeAction) {
106             if (!it()) {
107                 throw AssertionError(
108                     buildGeneralErrorMessage(
109                         "Failed to perform IME action, handler returned false.",
110                         selector,
111                         node
112                     )
113                 )
114             }
115         }
116     }
117 }
118 
SemanticsNodeInteractionnull119 private fun SemanticsNodeInteraction.getNodeAndFocus(
120     errorOnFail: String = "Failed to perform text input.",
121     requireEditable: Boolean = true
122 ): SemanticsNode {
123     tryPerformAccessibilityChecks()
124     val node = fetchSemanticsNode(errorOnFail)
125     assert(isEnabled()) { errorOnFail }
126     assert(hasRequestFocusAction()) { errorOnFail }
127     if (requireEditable) {
128         assert(hasSetTextAction()) { errorOnFail }
129         assert(hasInsertTextAtCursorAction()) { errorOnFail }
130     }
131 
132     if (!isFocused().matches(node)) {
133         // Get focus
134         performSemanticsAction(SemanticsActions.RequestFocus)
135     }
136 
137     return node
138 }
139 
wrapAssertionErrorsWithNodeInfonull140 internal expect inline fun <R> wrapAssertionErrorsWithNodeInfo(
141     selector: SemanticsSelector,
142     node: SemanticsNode,
143     block: () -> R
144 ): R
145