1 /*
2  * Copyright (C) 2022 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 com.android.cts.mocka11yime;
18 
19 import android.os.SystemClock;
20 import android.text.TextUtils;
21 import android.view.inputmethod.EditorInfo;
22 
23 import androidx.annotation.NonNull;
24 
25 import java.util.Optional;
26 import java.util.concurrent.TimeoutException;
27 import java.util.function.Predicate;
28 
29 /**
30  * Provides a set of utility methods to avoid boilerplate code when writing end-to-end tests.
31  */
32 public final class MockA11yImeEventStreamUtils {
33     private static final long TIME_SLICE = 50;  // msec
34 
35     /**
36      * Not intended to be instantiated.
37      */
MockA11yImeEventStreamUtils()38     private MockA11yImeEventStreamUtils() {
39     }
40 
41     /**
42      * Behavior mode of {@link #expectA11yImeEvent(MockA11yImeEventStream, Predicate,
43      * MockA11yImeEventStreamUtils.EventFilterMode, long)}
44      */
45     public enum EventFilterMode {
46         /**
47          * All {@link MockA11yImeEvent} events should be checked
48          */
49         CHECK_ALL,
50         /**
51          * Only events that return {@code true} from {@link MockA11yImeEvent#isEnterEvent()} should
52          * be checked
53          */
54         CHECK_ENTER_EVENT_ONLY,
55         /**
56          * Only events that return {@code false} from {@link MockA11yImeEvent#isEnterEvent()} should
57          * be checked
58          */
59         CHECK_EXIT_EVENT_ONLY,
60     }
61 
62     /**
63      * Wait until an event that matches the given {@code condition} is found in the stream.
64      *
65      * <p>When this method succeeds to find an event that matches the given {@code condition}, the
66      * stream position will be set to the next to the found object then the event found is returned.
67      * </p>
68      *
69      * <p>For convenience, this method automatically filter out exit events (events that return
70      * {@code false} from {@link MockA11yImeEvent#isEnterEvent()}.</p>
71      *
72      * @param stream {@link MockA11yImeEventStream} to be checked.
73      * @param condition the event condition to be matched
74      * @param timeout timeout in millisecond
75      * @return {@link MockA11yImeEvent} found
76      * @throws TimeoutException when the no event is matched to the given condition within
77      *                          {@code timeout}
78      */
79     @NonNull
expectA11yImeEvent(@onNull MockA11yImeEventStream stream, @NonNull Predicate<MockA11yImeEvent> condition, long timeout)80     public static MockA11yImeEvent expectA11yImeEvent(@NonNull MockA11yImeEventStream stream,
81             @NonNull Predicate<MockA11yImeEvent> condition, long timeout) throws TimeoutException {
82         return expectA11yImeEvent(stream, condition,
83                 MockA11yImeEventStreamUtils.EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout);
84     }
85 
86     /**
87      * Wait until an event that matches the given {@code condition} is found in the stream.
88      *
89      * <p>When this method succeeds to find an event that matches the given {@code condition}, the
90      * stream position will be set to the next to the found object then the event found is returned.
91      * </p>
92      *
93      * @param stream {@link MockA11yImeEventStream} to be checked.
94      * @param condition the event condition to be matched
95      * @param filterMode controls how events are filtered out
96      * @param timeout timeout in millisecond
97      * @return {@link MockA11yImeEvent} found
98      * @throws TimeoutException when the no event is matched to the given condition within
99      *                          {@code timeout}
100      */
101     @NonNull
expectA11yImeEvent(@onNull MockA11yImeEventStream stream, @NonNull Predicate<MockA11yImeEvent> condition, MockA11yImeEventStreamUtils.EventFilterMode filterMode, long timeout)102     public static MockA11yImeEvent expectA11yImeEvent(@NonNull MockA11yImeEventStream stream,
103             @NonNull Predicate<MockA11yImeEvent> condition,
104             MockA11yImeEventStreamUtils.EventFilterMode filterMode, long timeout)
105             throws TimeoutException {
106         while (true) {
107             if (timeout < 0) {
108                 throw new TimeoutException(
109                         "event not found within the timeout: " + stream.dump());
110             }
111             final Predicate<MockA11yImeEvent> combinedCondition;
112             switch (filterMode) {
113                 case CHECK_ALL:
114                     combinedCondition = condition;
115                     break;
116                 case CHECK_ENTER_EVENT_ONLY:
117                     combinedCondition = event -> event.isEnterEvent() && condition.test(event);
118                     break;
119                 case CHECK_EXIT_EVENT_ONLY:
120                     combinedCondition = event -> !event.isEnterEvent() && condition.test(event);
121                     break;
122                 default:
123                     throw new IllegalArgumentException("Unknown filterMode " + filterMode);
124             }
125             final Optional<MockA11yImeEvent> result = stream.seekToFirst(combinedCondition);
126             if (result.isPresent()) {
127                 stream.skip(1);
128                 return result.get();
129             }
130             SystemClock.sleep(TIME_SLICE);
131             timeout -= TIME_SLICE;
132         }
133     }
134 
135     /**
136      * Checks if {@code eventName} has occurred on the EditText(or TextView) of the current
137      * activity.
138      * @param eventName event name to check
139      * @param marker Test marker set to {@link android.widget.EditText#setPrivateImeOptions(String)}
140      * @return true if event occurred.
141      */
editorMatcherForA11yIme( @onNull String eventName, @NonNull String marker)142     public static Predicate<MockA11yImeEvent> editorMatcherForA11yIme(
143             @NonNull String eventName, @NonNull String marker) {
144         return event -> {
145             if (!TextUtils.equals(eventName, event.getEventName())) {
146                 return false;
147             }
148             final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo",
149                     EditorInfo.class);
150             return TextUtils.equals(marker, editorInfo.privateImeOptions);
151         };
152     }
153 
154 
155     /**
156      * Wait until an event that matches the given command is consumed by the MockA11yIme.
157      *
158      * <p>For convenience, this method automatically filter out enter events (events that return
159      * {@code true} from {@link MockA11yImeEvent#isEnterEvent()}.</p>
160      *
161      * @param stream {@link MockA11yImeEventStream} to be checked.
162      * @param command {@link MockA11yImeCommand} to be waited for.
163      * @param timeout timeout in millisecond
164      * @return {@link MockA11yImeEvent} found
165      * @throws TimeoutException when the no event is matched to the given condition within
166      *                          {@code timeout}
167      */
168     @NonNull
169     public static MockA11yImeEvent expectA11yImeCommand(@NonNull MockA11yImeEventStream stream,
170             @NonNull MockA11yImeCommand command, long timeout) throws TimeoutException {
171         final Predicate<MockA11yImeEvent> predicate = event -> {
172             if (!TextUtils.equals("onHandleCommand", event.getEventName())) {
173                 return false;
174             }
175             final MockA11yImeCommand eventCommand =
176                     MockA11yImeCommand.fromBundle(event.getArguments().getBundle("command"));
177             return eventCommand.getId() == command.getId();
178         };
179         return expectA11yImeEvent(stream, predicate,
180                 MockA11yImeEventStreamUtils.EventFilterMode.CHECK_EXIT_EVENT_ONLY, timeout);
181     }
182 
183     /**
184      * Assert that an event that matches the given {@code condition} will no be found in the stream
185      * within the given {@code timeout}.
186      *
187      * <p>When this method succeeds, the stream position will not change.</p>
188      *
189      * <p>For convenience, this method automatically filter out exit events (events that return
190      * {@code false} from {@link MockA11yImeEvent#isEnterEvent()}.</p>
191      *
192      * @param stream {@link MockA11yImeEventStream} to be checked.
193      * @param condition the event condition to be matched
194      * @param timeout timeout in millisecond
195      * @throws AssertionError if such an event is found within the given {@code timeout}
196      */
197     public static void notExpectA11yImeEvent(@NonNull MockA11yImeEventStream stream,
198             @NonNull Predicate<MockA11yImeEvent> condition, long timeout) {
199         notExpectA11yImeEvent(stream, condition,
200                 MockA11yImeEventStreamUtils.EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout);
201     }
202 
203     /**
204      * Assert that an event that matches the given {@code condition} will no be found in the stream
205      * within the given {@code timeout}.
206      *
207      * <p>When this method succeeds, the stream position will not change.</p>
208      *
209      * @param stream {@link MockA11yImeEventStream} to be checked.
210      * @param condition the event condition to be matched
211      * @param filterMode controls how events are filtered out
212      * @param timeout timeout in millisecond
213      * @throws AssertionError if such an event is found within the given {@code timeout}
214      */
215     public static void notExpectA11yImeEvent(@NonNull MockA11yImeEventStream stream,
216             @NonNull Predicate<MockA11yImeEvent> condition,
217             MockA11yImeEventStreamUtils.EventFilterMode filterMode, long timeout) {
218         final Predicate<MockA11yImeEvent> combinedCondition;
219         switch (filterMode) {
220             case CHECK_ALL:
221                 combinedCondition = condition;
222                 break;
223             case CHECK_ENTER_EVENT_ONLY:
224                 combinedCondition = event -> event.isEnterEvent() && condition.test(event);
225                 break;
226             case CHECK_EXIT_EVENT_ONLY:
227                 combinedCondition = event -> !event.isEnterEvent() && condition.test(event);
228                 break;
229             default:
230                 throw new IllegalArgumentException("Unknown filterMode " + filterMode);
231         }
232         while (true) {
233             if (timeout < 0) {
234                 return;
235             }
236             if (stream.findFirst(combinedCondition).isPresent()) {
237                 throw new AssertionError("notExpectEvent failed: " + stream.dump());
238             }
239             SystemClock.sleep(TIME_SLICE);
240             timeout -= TIME_SLICE;
241         }
242     }
243 }
244