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