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