1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.content.browser.accessibility;
6 
7 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
8 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
9 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
10 
11 import android.annotation.SuppressLint;
12 import android.annotation.TargetApi;
13 import android.graphics.RectF;
14 import android.os.Build;
15 import android.os.Bundle;
16 import android.text.InputType;
17 import android.text.Spannable;
18 import android.text.style.SuggestionSpan;
19 import android.view.View;
20 import android.view.ViewGroup;
21 import android.view.accessibility.AccessibilityEvent;
22 import android.view.accessibility.AccessibilityNodeInfo;
23 import android.view.accessibility.AccessibilityNodeProvider;
24 
25 import androidx.test.filters.LargeTest;
26 import androidx.test.filters.MediumTest;
27 
28 import org.hamcrest.Matchers;
29 import org.junit.Assert;
30 import org.junit.Ignore;
31 import org.junit.Rule;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 
35 import org.chromium.base.test.BaseJUnit4ClassRunner;
36 import org.chromium.base.test.util.Criteria;
37 import org.chromium.base.test.util.CriteriaHelper;
38 import org.chromium.base.test.util.MinAndroidSdkLevel;
39 import org.chromium.base.test.util.UrlUtils;
40 import org.chromium.content_public.browser.test.util.TestThreadUtils;
41 import org.chromium.content_shell_apk.ContentShellActivityTestRule;
42 
43 import java.lang.reflect.Method;
44 import java.util.concurrent.ExecutionException;
45 
46 /**
47  * Tests for WebContentsAccessibility. Actually tests WebContentsAccessibilityImpl that
48  * implements the interface.
49  */
50 @RunWith(BaseJUnit4ClassRunner.class)
51 public class WebContentsAccessibilityTest {
52     private interface AccessibilityNodeInfoMatcher {
matches(AccessibilityNodeInfo node)53         public boolean matches(AccessibilityNodeInfo node);
54     }
55 
56     private static class MutableInt {
MutableInt(int initialValue)57         public MutableInt(int initialValue) {
58             value = initialValue;
59         }
60 
61         public int value;
62     }
63 
64     // Constant from AccessibilityNodeInfo defined in the L SDK.
65     private static final int ACTION_SET_TEXT = 0x200000;
66 
67     // Member variables required for testing framework
68     private AccessibilityNodeProvider mNodeProvider;
69     private AccessibilityNodeInfo mNodeInfo;
70 
71     // Member variables used during unit tests involving single edit text field
72     private MutableInt mTraverseFromIndex = new MutableInt(-1);
73     private MutableInt mTraverseToIndex = new MutableInt(-1);
74     private MutableInt mSelectionFromIndex = new MutableInt(-1);
75     private MutableInt mSelectionToIndex = new MutableInt(-1);
76 
77     @Rule
78     public ContentShellActivityTestRule mActivityTestRule = new ContentShellActivityTestRule();
79 
80     /*
81      * Enable accessibility and wait until WebContentsAccessibility.getAccessibilityNodeProvider()
82      * returns something not null.
83      */
enableAccessibilityAndWaitForNodeProvider()84     private AccessibilityNodeProvider enableAccessibilityAndWaitForNodeProvider() {
85         final WebContentsAccessibilityImpl wcax = mActivityTestRule.getWebContentsAccessibility();
86         wcax.setState(true);
87         wcax.setAccessibilityEnabledForTesting();
88 
89         CriteriaHelper.pollUiThread(() -> {
90             Criteria.checkThat(wcax.getAccessibilityNodeProvider(), Matchers.notNullValue());
91         });
92 
93         return wcax.getAccessibilityNodeProvider();
94     }
95 
96     /**
97      * Helper method to call AccessibilityNodeInfo.getChildId and convert to a virtual
98      * view ID using reflection, since the needed methods are hidden.
99      */
getChildId(AccessibilityNodeInfo node, int index)100     private int getChildId(AccessibilityNodeInfo node, int index) {
101         try {
102             Method getChildIdMethod =
103                     AccessibilityNodeInfo.class.getMethod("getChildId", int.class);
104             long childId = (long) getChildIdMethod.invoke(node, Integer.valueOf(index));
105             Method getVirtualDescendantIdMethod =
106                     AccessibilityNodeInfo.class.getMethod("getVirtualDescendantId", long.class);
107             int virtualViewId =
108                     (int) getVirtualDescendantIdMethod.invoke(null, Long.valueOf(childId));
109             return virtualViewId;
110         } catch (Exception ex) {
111             Assert.fail("Unable to call hidden AccessibilityNodeInfo method: " + ex.toString());
112             return 0;
113         }
114     }
115 
116     /**
117      * Helper method to recursively search a tree of virtual views under an
118      * AccessibilityNodeProvider and return one whose text or contentDescription equals |text|.
119      * Returns the virtual view ID of the matching node, if found, and View.NO_ID if not.
120      */
findNodeMatching(AccessibilityNodeProvider provider, int virtualViewId, AccessibilityNodeInfoMatcher matcher)121     private int findNodeMatching(AccessibilityNodeProvider provider, int virtualViewId,
122             AccessibilityNodeInfoMatcher matcher) {
123         AccessibilityNodeInfo node = provider.createAccessibilityNodeInfo(virtualViewId);
124         Assert.assertNotEquals(node, null);
125 
126         if (matcher.matches(node)) return virtualViewId;
127 
128         for (int i = 0; i < node.getChildCount(); i++) {
129             int childId = getChildId(node, i);
130             AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(childId);
131             if (child != null) {
132                 int result = findNodeMatching(provider, childId, matcher);
133                 if (result != View.NO_ID) return result;
134             }
135         }
136 
137         return View.NO_ID;
138     }
139 
140     /**
141      * Helper method to block until findNodeMatching() returns a valid node matching
142      * the given criteria. Returns the virtual view ID of the matching node, if found, and
143      * asserts if not.
144      */
waitForNodeMatching( AccessibilityNodeProvider provider, AccessibilityNodeInfoMatcher matcher)145     private int waitForNodeMatching(
146             AccessibilityNodeProvider provider, AccessibilityNodeInfoMatcher matcher) {
147         CriteriaHelper.pollUiThread(() -> {
148             Criteria.checkThat(
149                     findNodeMatching(provider, View.NO_ID, matcher), Matchers.not(View.NO_ID));
150         });
151 
152         int virtualViewId = TestThreadUtils.runOnUiThreadBlockingNoException(
153                 () -> findNodeMatching(provider, View.NO_ID, matcher));
154         Assert.assertNotEquals(View.NO_ID, virtualViewId);
155         return virtualViewId;
156     }
157 
158     /**
159      * Block until the tree of virtual views under |mNodeProvider| has a node whose
160      * text or contentDescription equals |text|. Returns the virtual view ID of
161      * the matching node, if found, and asserts if not.
162      */
waitForNodeWithText(AccessibilityNodeProvider provider, String text)163     private int waitForNodeWithText(AccessibilityNodeProvider provider, String text) {
164         return waitForNodeMatching(provider, new AccessibilityNodeInfoMatcher() {
165             @Override
166             public boolean matches(AccessibilityNodeInfo node) {
167                 return text.equals(node.getText()) || text.equals(node.getContentDescription());
168             }
169         });
170     }
171 
172     /**
173      * Block until the tree of virtual views under |mNodeProvider| has a node whose
174      * text equals |text|. Returns the virtual view ID of the matching node, or asserts if not.
175      */
176     private int waitForNodeWithTextOnly(AccessibilityNodeProvider provider, String text) {
177         return waitForNodeMatching(provider, new AccessibilityNodeInfoMatcher() {
178             @Override
179             public boolean matches(AccessibilityNodeInfo node) {
180                 return text.equals(node.getText());
181             }
182         });
183     }
184 
185     /**
186      * Block until the tree of virtual views under |mNodeProvider| has a node whose input type
187      * is |type|. Returns the virtual view ID of the matching node, if found, and asserts if not.
188      */
189     @SuppressLint("NewApi")
190     private int waitForNodeWithTextInputType(AccessibilityNodeProvider provider, int type) {
191         return waitForNodeMatching(provider, new AccessibilityNodeInfoMatcher() {
192             @Override
193             public boolean matches(AccessibilityNodeInfo node) {
194                 return node.getInputType() == type;
195             }
196         });
197     }
198 
199     /**
200      * Block until the tree of virtual views under |mNodeProvider| has a node whose className equals
201      * |className|. Returns the virtual view ID of the matching node, if found, and asserts if not.
202      */
203     private int waitForNodeWithClassName(AccessibilityNodeProvider provider, String className) {
204         return waitForNodeMatching(provider, new AccessibilityNodeInfoMatcher() {
205             @Override
206             public boolean matches(AccessibilityNodeInfo node) {
207                 return className.equals(node.getClassName());
208             }
209         });
210     }
211 
212     /**
213      * Helper method to perform actions on the UI so we can then send accessibility events
214      *
215      * @param viewId int                            viewId set during setUpEditTextDelegate
216      * @param action int                            desired AccessibilityNodeInfo action
217      * @param args Bundle                           action bundle
218      * @return boolean                              return value of performAction
219      * @throws ExecutionException                   Error
220      */
221     private boolean performActionOnUiThread(int viewId, int action, Bundle args)
222             throws ExecutionException {
223         return TestThreadUtils.runOnUiThreadBlocking(
224                 () -> mNodeProvider.performAction(viewId, action, args));
225     }
226 
227     /**
228      * Helper method to build a web page with an edit text for our set of tests, and find the
229      * virtualViewId of that given field and return it
230      *
231      * @param htmlContent String        content of the web page
232      * @param editTextValue String      value of the edit text field to find
233      * @return int                      virtualViewId of the edit text identified
234      */
235     private int buildWebPageWithEditText(String htmlContent, String editTextValue) {
236         // Load a simple page with an input and the text "Testing"
237         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(htmlContent));
238         mActivityTestRule.waitForActiveShellToBeDoneLoading();
239         mNodeProvider = enableAccessibilityAndWaitForNodeProvider();
240 
241         // Find a node in the accessibility tree with input type TYPE_CLASS_TEXT.
242         int editTextVirtualViewId =
243                 waitForNodeWithTextInputType(mNodeProvider, InputType.TYPE_CLASS_TEXT);
244         mNodeInfo = mNodeProvider.createAccessibilityNodeInfo(editTextVirtualViewId);
245 
246         // Assert we have got the correct node.
247         Assert.assertNotEquals(mNodeInfo, null);
248         Assert.assertEquals(mNodeInfo.getInputType(), InputType.TYPE_CLASS_TEXT);
249         Assert.assertEquals(mNodeInfo.getText().toString(), editTextValue);
250 
251         return editTextVirtualViewId;
252     }
253 
254     /**
255      * Helper method to build a web page with a contenteditable for our set of tests, and find the
256      * virtualViewId of that given div and return it
257      *
258      * @param htmlContent String                content of the web page
259      * @param contenteditableText String        value of the contenteditable div to find
260      * @return int                              virtualViewId of the contenteditable identified
261      */
262     private int buildWebPageWithContentEditable(String htmlContent, String contenteditableText) {
263         // Load a simple page with an input and the text "Testing"
264         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(htmlContent));
265         mActivityTestRule.waitForActiveShellToBeDoneLoading();
266         mNodeProvider = enableAccessibilityAndWaitForNodeProvider();
267 
268         // Find a node in the accessibility tree with input type TYPE_CLASS_TEXT.
269         int contenteditableVirtualViewId =
270                 waitForNodeWithClassName(mNodeProvider, "android.widget.EditText");
271         mNodeInfo = mNodeProvider.createAccessibilityNodeInfo(contenteditableVirtualViewId);
272 
273         // Assert we have got the correct node.
274         Assert.assertNotEquals(mNodeInfo, null);
275         Assert.assertEquals(contenteditableText, mNodeInfo.getText().toString());
276 
277         return contenteditableVirtualViewId;
278     }
279 
280     /**
281      * Helper method to set up delegates on an edit text for testing. This is used in the tests
282      * below that check our accessibility events are properly indexed. The editTextVirtualViewId
283      * parameter should be the value returned from buildWebPageWithEditText
284      *
285      * @param editTextVirtualViewId int     virtualViewId of EditText to setup delegates on
286      * @throws Throwable Error
287      */
288     private void setUpEditTextDelegate(int editTextVirtualViewId) throws Throwable {
289         // Add an accessibility delegate to capture TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY and
290         // TYPE_VIEW_TEXT_SELECTION_CHANGED events and store their ToIndex and FromIndex.
291         // The delegate is set on the parent as WebContentsAccessibilityImpl sends events using the
292         // parent.
293         ((ViewGroup) mActivityTestRule.getContainerView().getParent())
294                 .setAccessibilityDelegate(new View.AccessibilityDelegate() {
295                     @Override
296                     public boolean onRequestSendAccessibilityEvent(
297                             ViewGroup host, View child, AccessibilityEvent event) {
298                         if (event.getEventType()
299                                 == AccessibilityEvent
300                                            .TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY) {
301                             mTraverseFromIndex.value = event.getFromIndex();
302                             mTraverseToIndex.value = event.getToIndex();
303                         } else if (event.getEventType()
304                                 == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
305                             mSelectionFromIndex.value = event.getFromIndex();
306                             mSelectionToIndex.value = event.getToIndex();
307                         }
308 
309                         // Return false so that an accessibility event is not actually sent.
310                         return false;
311                     }
312                 });
313 
314         // Focus our field
315         boolean result1 = performActionOnUiThread(
316                 editTextVirtualViewId, AccessibilityNodeInfo.ACTION_FOCUS, null);
317         boolean result2 = performActionOnUiThread(
318                 editTextVirtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
319 
320         // Assert all actions are performed successfully.
321         Assert.assertTrue(result1);
322         Assert.assertTrue(result2);
323 
324         while (!mNodeInfo.isFocused()) {
325             Thread.sleep(100);
326             mNodeInfo.recycle();
327             mNodeInfo = mNodeProvider.createAccessibilityNodeInfo(editTextVirtualViewId);
328         }
329     }
330 
331     /**
332      * Helper method to refresh the |mNodeInfo| object by recycling, waiting 1 sec, then refresh.
333      */
334     private void refreshMNodeInfo(int virtualViewId) throws InterruptedException {
335         mNodeInfo.recycle();
336         Thread.sleep(1000);
337         mNodeInfo = mNodeProvider.createAccessibilityNodeInfo(virtualViewId);
338     }
339 
340     /**
341      * Helper method to tear down the setup of our tests so we can start the next test clean
342      */
343     private void tearDown() {
344         mNodeProvider = null;
345         mNodeInfo = null;
346 
347         mTraverseFromIndex.value = -1;
348         mTraverseToIndex.value = -1;
349         mSelectionFromIndex.value = -1;
350         mSelectionToIndex.value = -1;
351     }
352 
353     /**
354      * Ensure traverse events and selection events are properly indexed when navigating an edit
355      * field by character with selection mode off
356      */
357     @Test
358     @MediumTest
359     public void testEventIndices_SelectionOFF_CharacterGranularity() throws Throwable {
360         // Build a simple web page with an input and the text "Testing"
361         int editTextVirtualViewId = buildWebPageWithEditText(
362                 "<input id=\"fn\" type=\"text\" value=\"Testing\">", "Testing");
363 
364         setUpEditTextDelegate(editTextVirtualViewId);
365 
366         // Set granularity to CHARACTER, with selection FALSE
367         Bundle args = new Bundle();
368         args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
369                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
370         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
371 
372         // Simulate swiping left (backward)
373         for (int i = 7; i > 0; i--) {
374             performActionOnUiThread(editTextVirtualViewId,
375                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
376 
377             Assert.assertEquals(i - 1, mTraverseFromIndex.value);
378             Assert.assertEquals(i, mTraverseToIndex.value);
379             Assert.assertEquals(i - 1, mSelectionFromIndex.value);
380             Assert.assertEquals(i - 1, mSelectionToIndex.value);
381         }
382 
383         // Simulate swiping right (forward)
384         for (int i = 0; i < 7; i++) {
385             performActionOnUiThread(editTextVirtualViewId,
386                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
387 
388             Assert.assertEquals(i, mTraverseFromIndex.value);
389             Assert.assertEquals(i + 1, mTraverseToIndex.value);
390             Assert.assertEquals(i + 1, mSelectionFromIndex.value);
391             Assert.assertEquals(i + 1, mSelectionToIndex.value);
392         }
393 
394         tearDown();
395     }
396 
397     /**
398      * Ensure traverse events and selection events are properly indexed when navigating an edit
399      * field by character with selection mode on
400      */
401     @Test
402     @LargeTest
403     @Ignore("Skipping due to long run time")
404     public void testEventIndices_SelectionON_CharacterGranularity() throws Throwable {
405         // Build a simple web page with an input and the text "Testing"
406         int editTextVirtualViewId = buildWebPageWithEditText(
407                 "<input id=\"fn\" type=\"text\" value=\"Testing\">", "Testing");
408 
409         setUpEditTextDelegate(editTextVirtualViewId);
410 
411         // Set granularity to CHARACTER, with selection TRUE
412         Bundle args = new Bundle();
413         args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
414                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
415         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
416 
417         // Simulate swiping left (backward) (adds to selections)
418         for (int i = 7; i > 0; i--) {
419             performActionOnUiThread(editTextVirtualViewId,
420                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
421 
422             Assert.assertEquals(i - 1, mTraverseFromIndex.value);
423             Assert.assertEquals(i, mTraverseToIndex.value);
424             Assert.assertEquals(7, mSelectionFromIndex.value);
425             Assert.assertEquals(i - 1, mSelectionToIndex.value);
426 
427             refreshMNodeInfo(editTextVirtualViewId);
428 
429             Assert.assertTrue(mNodeInfo.isEditable());
430             Assert.assertEquals(i - 1, mNodeInfo.getTextSelectionStart());
431             Assert.assertEquals(7, mNodeInfo.getTextSelectionEnd());
432         }
433 
434         // Simulate swiping right (forward) (removes from selection)
435         for (int i = 0; i < 7; i++) {
436             performActionOnUiThread(editTextVirtualViewId,
437                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
438 
439             Assert.assertEquals(i, mTraverseFromIndex.value);
440             Assert.assertEquals(i + 1, mTraverseToIndex.value);
441             Assert.assertEquals(7, mSelectionFromIndex.value);
442             Assert.assertEquals(i + 1, mSelectionToIndex.value);
443 
444             refreshMNodeInfo(editTextVirtualViewId);
445 
446             Assert.assertTrue(mNodeInfo.isEditable());
447             Assert.assertEquals(i + 1, mNodeInfo.getTextSelectionStart());
448             Assert.assertEquals(7, mNodeInfo.getTextSelectionEnd());
449         }
450 
451         // Turn selection mode off and traverse to beginning so we can select forwards
452         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
453         for (int i = 7; i > 0; i--) {
454             performActionOnUiThread(editTextVirtualViewId,
455                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
456         }
457 
458         // Turn selection mode on
459         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
460 
461         // Simulate swiping right (forward) (adds to selection)
462         for (int i = 0; i < 7; i++) {
463             performActionOnUiThread(editTextVirtualViewId,
464                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
465 
466             Assert.assertEquals(i, mTraverseFromIndex.value);
467             Assert.assertEquals(i + 1, mTraverseToIndex.value);
468             Assert.assertEquals(0, mSelectionFromIndex.value);
469             Assert.assertEquals(i + 1, mSelectionToIndex.value);
470 
471             refreshMNodeInfo(editTextVirtualViewId);
472 
473             Assert.assertTrue(mNodeInfo.isEditable());
474             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
475             Assert.assertEquals(i + 1, mNodeInfo.getTextSelectionEnd());
476         }
477 
478         // Simulate swiping left (backward) (removes from selections)
479         for (int i = 7; i > 0; i--) {
480             performActionOnUiThread(editTextVirtualViewId,
481                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
482 
483             Assert.assertEquals(i - 1, mTraverseFromIndex.value);
484             Assert.assertEquals(i, mTraverseToIndex.value);
485             Assert.assertEquals(0, mSelectionFromIndex.value);
486             Assert.assertEquals(i - 1, mSelectionToIndex.value);
487 
488             refreshMNodeInfo(editTextVirtualViewId);
489 
490             Assert.assertTrue(mNodeInfo.isEditable());
491             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
492             Assert.assertEquals(i - 1, mNodeInfo.getTextSelectionEnd());
493         }
494 
495         tearDown();
496     }
497 
498     /**
499      * Ensure traverse events and selection events are properly indexed when navigating an edit
500      * field by word with selection mode off
501      */
502     @Test
503     @MediumTest
504     public void testEventIndices_SelectionOFF_WordGranularity() throws Throwable {
505         // Build a simple web page with an input and the text "Testing this output is correct"
506         int editTextVirtualViewId = buildWebPageWithEditText(
507                 "<input id=\"fn\" type=\"text\" value=\"Testing this output is correct\">",
508                 "Testing this output is correct");
509 
510         setUpEditTextDelegate(editTextVirtualViewId);
511 
512         // Set granularity to WORD, with selection FALSE
513         Bundle args = new Bundle();
514         args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
515                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
516         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
517 
518         int[] wordStarts = new int[] {0, 8, 13, 20, 23};
519         int[] wordEnds = new int[] {7, 12, 19, 22, 30};
520 
521         // Simulate swiping left (backward) through all 5 words, check indices along the way
522         for (int i = 4; i >= 0; --i) {
523             performActionOnUiThread(editTextVirtualViewId,
524                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
525 
526             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
527             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
528             Assert.assertEquals(wordStarts[i], mSelectionFromIndex.value);
529             Assert.assertEquals(wordStarts[i], mSelectionToIndex.value);
530         }
531 
532         // Simulate swiping right (forward) through all 5 words, check indices along the way
533         for (int i = 0; i < 5; ++i) {
534             performActionOnUiThread(editTextVirtualViewId,
535                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
536 
537             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
538             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
539             Assert.assertEquals(wordEnds[i], mSelectionFromIndex.value);
540             Assert.assertEquals(wordEnds[i], mSelectionToIndex.value);
541         }
542 
543         tearDown();
544     }
545 
546     /**
547      * Ensure traverse events and selection events are properly indexed when navigating an edit
548      * field by word with selection mode on
549      */
550     @Test
551     @LargeTest
552     @Ignore("Skipping due to long run time")
553     public void testEventIndices_SelectionON_WordGranularity() throws Throwable {
554         // Build a simple web page with an input and the text "Testing this output is correct"
555         int editTextVirtualViewId = buildWebPageWithEditText(
556                 "<input id=\"fn\" type=\"text\" value=\"Testing this output is correct\">",
557                 "Testing this output is correct");
558 
559         setUpEditTextDelegate(editTextVirtualViewId);
560 
561         // Set granularity to WORD, with selection TRUE
562         Bundle args = new Bundle();
563         args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
564                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
565         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
566 
567         int[] wordStarts = new int[] {0, 8, 13, 20, 23};
568         int[] wordEnds = new int[] {7, 12, 19, 22, 30};
569 
570         // Simulate swiping left (backward, adds to selection) through all 5 words, check indices
571         for (int i = 4; i >= 0; --i) {
572             performActionOnUiThread(editTextVirtualViewId,
573                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
574 
575             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
576             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
577             Assert.assertEquals(30, mSelectionFromIndex.value);
578             Assert.assertEquals(wordStarts[i], mSelectionToIndex.value);
579 
580             refreshMNodeInfo(editTextVirtualViewId);
581 
582             Assert.assertTrue(mNodeInfo.isEditable());
583             Assert.assertEquals(wordStarts[i], mNodeInfo.getTextSelectionStart());
584             Assert.assertEquals(30, mNodeInfo.getTextSelectionEnd());
585         }
586 
587         // Simulate swiping right (forward, removes selection) through all 5 words, check indices
588         for (int i = 0; i < 5; ++i) {
589             performActionOnUiThread(editTextVirtualViewId,
590                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
591 
592             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
593             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
594             Assert.assertEquals(30, mSelectionFromIndex.value);
595             Assert.assertEquals(wordEnds[i], mSelectionToIndex.value);
596 
597             refreshMNodeInfo(editTextVirtualViewId);
598 
599             Assert.assertTrue(mNodeInfo.isEditable());
600             Assert.assertEquals(wordEnds[i], mNodeInfo.getTextSelectionStart());
601             Assert.assertEquals(30, mNodeInfo.getTextSelectionEnd());
602         }
603 
604         // Turn selection mode off and traverse to beginning so we can select forwards
605         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
606         for (int i = 4; i >= 0; i--) {
607             performActionOnUiThread(editTextVirtualViewId,
608                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
609         }
610 
611         // Turn selection mode on
612         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
613 
614         // Simulate swiping right (forward) (adds to selection)
615         for (int i = 0; i < 5; ++i) {
616             performActionOnUiThread(editTextVirtualViewId,
617                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
618 
619             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
620             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
621             Assert.assertEquals(0, mSelectionFromIndex.value);
622             Assert.assertEquals(wordEnds[i], mSelectionToIndex.value);
623 
624             refreshMNodeInfo(editTextVirtualViewId);
625 
626             Assert.assertTrue(mNodeInfo.isEditable());
627             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
628             Assert.assertEquals(wordEnds[i], mNodeInfo.getTextSelectionEnd());
629         }
630 
631         // Simulate swiping left (backward) (removes from selections)
632         for (int i = 4; i >= 0; --i) {
633             performActionOnUiThread(editTextVirtualViewId,
634                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
635 
636             Assert.assertEquals(wordStarts[i], mTraverseFromIndex.value);
637             Assert.assertEquals(wordEnds[i], mTraverseToIndex.value);
638             Assert.assertEquals(0, mSelectionFromIndex.value);
639             Assert.assertEquals(wordStarts[i], mSelectionToIndex.value);
640 
641             refreshMNodeInfo(editTextVirtualViewId);
642 
643             Assert.assertTrue(mNodeInfo.isEditable());
644             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
645             Assert.assertEquals(wordStarts[i], mNodeInfo.getTextSelectionEnd());
646         }
647 
648         tearDown();
649     }
650 
651     /**
652      * Ensure traverse events and selection events are properly indexed when navigating a
653      * contenteditable by character with selection mode on.
654      */
655     @Test
656     @LargeTest
657     @Ignore("Skipping due to long run time")
658     public void testEventIndices_contenteditable_SelectionON_CharacterGranularity()
659             throws Throwable {
660         int contentEditableVirtualViewId =
661                 buildWebPageWithContentEditable("<div contenteditable>Testing</div>", "Testing");
662 
663         setUpEditTextDelegate(contentEditableVirtualViewId);
664 
665         // Move cursor to the end of the field for consistency.
666         Bundle moveArgs = new Bundle();
667         moveArgs.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
668                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
669         for (int i = 7; i > 0; i--) {
670             performActionOnUiThread(contentEditableVirtualViewId,
671                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveArgs);
672         }
673 
674         // Set granularity to CHARACTER, with selection TRUE
675         Bundle args = new Bundle();
676         args.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
677                 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
678         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
679 
680         // Simulate swiping left (backward) (adds to selections)
681         for (int i = 7; i > 0; i--) {
682             performActionOnUiThread(contentEditableVirtualViewId,
683                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
684 
685             Assert.assertEquals(i - 1, mTraverseFromIndex.value);
686             Assert.assertEquals(i, mTraverseToIndex.value);
687             Assert.assertEquals(7, mSelectionFromIndex.value);
688             Assert.assertEquals(i - 1, mSelectionToIndex.value);
689 
690             refreshMNodeInfo(contentEditableVirtualViewId);
691 
692             Assert.assertTrue(mNodeInfo.isEditable());
693             Assert.assertEquals(i - 1, mNodeInfo.getTextSelectionEnd());
694             Assert.assertEquals(7, mNodeInfo.getTextSelectionStart());
695         }
696 
697         // Simulate swiping right (forward) (removes from selection)
698         for (int i = 0; i < 7; i++) {
699             performActionOnUiThread(contentEditableVirtualViewId,
700                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
701 
702             Assert.assertEquals(i, mTraverseFromIndex.value);
703             Assert.assertEquals(i + 1, mTraverseToIndex.value);
704             Assert.assertEquals(7, mSelectionFromIndex.value);
705             Assert.assertEquals(i + 1, mSelectionToIndex.value);
706 
707             refreshMNodeInfo(contentEditableVirtualViewId);
708 
709             Assert.assertTrue(mNodeInfo.isEditable());
710             Assert.assertEquals(i + 1, mNodeInfo.getTextSelectionEnd());
711             Assert.assertEquals(7, mNodeInfo.getTextSelectionStart());
712         }
713 
714         // Turn selection mode off and traverse to beginning so we can select forwards
715         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
716         for (int i = 7; i > 0; i--) {
717             performActionOnUiThread(contentEditableVirtualViewId,
718                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
719         }
720 
721         // Turn selection mode on
722         args.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
723 
724         // Simulate swiping right (forward) (adds to selection)
725         for (int i = 0; i < 7; i++) {
726             performActionOnUiThread(contentEditableVirtualViewId,
727                     AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
728 
729             Assert.assertEquals(i, mTraverseFromIndex.value);
730             Assert.assertEquals(i + 1, mTraverseToIndex.value);
731             Assert.assertEquals(0, mSelectionFromIndex.value);
732             Assert.assertEquals(i + 1, mSelectionToIndex.value);
733 
734             refreshMNodeInfo(contentEditableVirtualViewId);
735 
736             Assert.assertTrue(mNodeInfo.isEditable());
737             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
738             Assert.assertEquals(i + 1, mNodeInfo.getTextSelectionEnd());
739         }
740 
741         // Simulate swiping left (backward) (removes from selections)
742         for (int i = 7; i > 0; i--) {
743             performActionOnUiThread(contentEditableVirtualViewId,
744                     AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
745 
746             Assert.assertEquals(i - 1, mTraverseFromIndex.value);
747             Assert.assertEquals(i, mTraverseToIndex.value);
748             Assert.assertEquals(0, mSelectionFromIndex.value);
749             Assert.assertEquals(i - 1, mSelectionToIndex.value);
750 
751             refreshMNodeInfo(contentEditableVirtualViewId);
752 
753             Assert.assertTrue(mNodeInfo.isEditable());
754             Assert.assertEquals(0, mNodeInfo.getTextSelectionStart());
755             Assert.assertEquals(i - 1, mNodeInfo.getTextSelectionEnd());
756         }
757 
758         tearDown();
759     }
760 
761     /**
762      * Text fields should expose ACTION_SET_TEXT
763      */
764     @Test
765     @MediumTest
766     public void testTextFieldExposesActionSetText() {
767         // Load a web page with a text field.
768         final String data = "<h1>Simple test page</h1>"
769                 + "<section><input type=text placeholder=Text></section>";
770         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
771         mActivityTestRule.waitForActiveShellToBeDoneLoading();
772         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
773         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
774         AccessibilityNodeInfo textNode =
775                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
776         Assert.assertNotEquals(textNode, null);
777         for (AccessibilityNodeInfo.AccessibilityAction action : textNode.getActionList()) {
778             if (action.getId() == ACTION_SET_TEXT) return;
779         }
780         Assert.fail("ACTION_SET_TEXT not found");
781     }
782 
783     /**
784      * ContentEditable elements should get a class name of EditText.
785      **/
786     @Test
787     @MediumTest
788     public void testContentEditableClassName() {
789         final String data = "<div contenteditable>Edit This</div>";
790 
791         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
792         mActivityTestRule.waitForActiveShellToBeDoneLoading();
793         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
794         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
795         AccessibilityNodeInfo editableNode =
796                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
797         Assert.assertNotNull(editableNode);
798         Assert.assertEquals(editableNode.isEditable(), true);
799         Assert.assertEquals(editableNode.getText().toString(), "Edit This");
800     }
801 
802     /**
803      * Tests presence of ContentInvalid attribute and correctness of
804      * error message given aria-invalid = true
805      **/
806     @Test
807     @MediumTest
808     public void testEditTextFieldAriaInvalidTrueErrorMessage() {
809         final String data = "<form>\n"
810                 + "  First name:<br>\n"
811                 + "  <input id='fn' type='text' aria-invalid='true'><br>\n"
812                 + "<input type='submit'><br>"
813                 + "</form>";
814         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
815         mActivityTestRule.waitForActiveShellToBeDoneLoading();
816         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
817         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
818         AccessibilityNodeInfo textNode =
819                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
820         Assert.assertNotEquals(textNode, null);
821         Assert.assertEquals(textNode.isContentInvalid(), true);
822         Assert.assertEquals(textNode.getError(), "Invalid entry");
823     }
824 
825     /**
826      * Tests presence of ContentInvalid attribute and correctness of
827      * error message given aria-invalid = spelling
828      **/
829     @Test
830     @MediumTest
831     public void testEditTextFieldAriaInvalidSpellingErrorMessage() {
832         final String data = "<input type='text' aria-invalid='spelling'><br>\n";
833 
834         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
835         mActivityTestRule.waitForActiveShellToBeDoneLoading();
836         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
837         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
838         AccessibilityNodeInfo textNode =
839                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
840         Assert.assertNotEquals(textNode, null);
841         Assert.assertEquals(textNode.isContentInvalid(), true);
842         Assert.assertEquals(textNode.getError(), "Invalid spelling");
843     }
844 
845     /**
846      * Tests presence of ContentInvalid attribute and correctness of
847      * error message given aria-invalid = grammar
848      **/
849     @Test
850     @MediumTest
851     public void testEditTextFieldAriaInvalidGrammarErrorMessage() {
852         final String data = "<input type='text' aria-invalid='grammar'><br>\n";
853 
854         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
855         mActivityTestRule.waitForActiveShellToBeDoneLoading();
856         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
857         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
858         AccessibilityNodeInfo textNode =
859                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
860         Assert.assertNotEquals(textNode, null);
861         Assert.assertEquals(textNode.isContentInvalid(), true);
862         Assert.assertEquals(textNode.getError(), "Invalid grammar");
863     }
864 
865     /**
866      * Tests ContentInvalid is false and empty error message for well-formed input
867      **/
868     @Test
869     @MediumTest
870     public void testEditTextFieldValidNoErrorMessage() {
871         final String data = "<input type='text'><br>\n";
872         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
873         mActivityTestRule.waitForActiveShellToBeDoneLoading();
874         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
875         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
876 
877         AccessibilityNodeInfo textNode =
878                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
879         Assert.assertNotEquals(null, textNode);
880         Assert.assertFalse(textNode.isContentInvalid());
881         Assert.assertNull(textNode.getError());
882     }
883 
884     /**
885      * Test spelling error is encoded as a Spannable.
886      **/
887     @Test
888     @MediumTest
889     public void testSpellingError() {
890         // Load a web page containing a text field with one misspelling.
891         // Note that for content_shell, no spelling suggestions are enabled
892         // by default.
893         final String data = "<input type='text' value='one wordd has an error'>";
894         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
895         mActivityTestRule.waitForActiveShellToBeDoneLoading();
896         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
897         int textNodeVirtualViewId = waitForNodeWithClassName(provider, "android.widget.EditText");
898 
899         // Call a test API to explicitly add a spelling error in the same format as
900         // would be generated if spelling correction was enabled.
901         final WebContentsAccessibilityImpl wcax = mActivityTestRule.getWebContentsAccessibility();
902         wcax.addSpellingErrorForTesting(textNodeVirtualViewId, 4, 9);
903 
904         // Clear our cache for this node.
905         wcax.clearNodeInfoCacheForGivenId(textNodeVirtualViewId);
906 
907         // Now get that AccessibilityNodeInfo and retrieve its text.
908         AccessibilityNodeInfo textNode =
909                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
910         Assert.assertNotEquals(textNode, null);
911         CharSequence text = textNode.getText();
912         Assert.assertEquals(text.toString(), "one wordd has an error");
913 
914         // Assert that the text has a SuggestionSpan surrounding the proper word.
915         Assert.assertTrue(text instanceof Spannable);
916         Spannable spannable = (Spannable) text;
917         Object spans[] = spannable.getSpans(0, text.length(), Object.class);
918         boolean foundSuggestionSpan = false;
919         for (Object span : spans) {
920             if (span instanceof SuggestionSpan) {
921                 Assert.assertEquals(4, spannable.getSpanStart(span));
922                 Assert.assertEquals(9, spannable.getSpanEnd(span));
923                 foundSuggestionSpan = true;
924             }
925         }
926         Assert.assertTrue(foundSuggestionSpan);
927     }
928 
929     /**
930      * Test Android O API to retrieve character bounds from an accessible node.
931      */
932     @Test
933     @MediumTest
934     @MinAndroidSdkLevel(Build.VERSION_CODES.O)
935     @TargetApi(Build.VERSION_CODES.O)
936     public void testAddExtraDataToAccessibilityNodeInfo() {
937         // Load a really simple webpage.
938         final String data = "<h1>Simple test page</h1>"
939                 + "<section><p>Text</p></section>";
940         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(data));
941         mActivityTestRule.waitForActiveShellToBeDoneLoading();
942         AccessibilityNodeProvider provider = enableAccessibilityAndWaitForNodeProvider();
943 
944         // Wait until we find a node in the accessibility tree with the text "Text".
945         final int textNodeVirtualViewId = waitForNodeWithText(provider, "Text");
946 
947         // Now call the API we want to test - addExtraDataToAccessibilityNodeInfo.
948         final AccessibilityNodeInfo initialTextNode =
949                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
950         Assert.assertNotEquals(initialTextNode, null);
951         final Bundle arguments = new Bundle();
952         arguments.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0);
953         arguments.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, 4);
954 
955         // addExtraDataToAccessibilityNodeInfo() will end up calling RenderFrameHostImpl's method
956         // AccessibilityPerformAction() in the C++ code, which needs to be run from the UI thread.
957         TestThreadUtils.runOnUiThreadBlocking(new Runnable() {
958             @Override
959             public void run() {
960                 provider.addExtraDataToAccessibilityNodeInfo(textNodeVirtualViewId, initialTextNode,
961                         EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, arguments);
962             }
963         });
964 
965         // It should return a result, but all of the rects will be the same because it hasn't
966         // loaded inline text boxes yet.
967         Bundle extras = initialTextNode.getExtras();
968         RectF[] result =
969                 (RectF[]) extras.getParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
970         Assert.assertNotEquals(result, null);
971         Assert.assertEquals(result.length, 4);
972         Assert.assertEquals(result[0], result[1]);
973         Assert.assertEquals(result[0], result[2]);
974         Assert.assertEquals(result[0], result[3]);
975 
976         // The role string should be a camel cased programmatic identifier.
977         CharSequence roleString = extras.getCharSequence("AccessibilityNodeInfo.chromeRole");
978         Assert.assertEquals("paragraph", roleString.toString());
979 
980         // The data needed for text character locations loads asynchronously. Block until
981         // it successfully returns the character bounds.
982         CriteriaHelper.pollUiThread(() -> {
983             AccessibilityNodeInfo textNode =
984                     provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
985             provider.addExtraDataToAccessibilityNodeInfo(textNodeVirtualViewId, textNode,
986                     EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, arguments);
987             Bundle textNodeExtras = textNode.getExtras();
988             RectF[] textNodeResults = (RectF[]) textNodeExtras.getParcelableArray(
989                     EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
990             Criteria.checkThat(textNodeResults, Matchers.arrayWithSize(4));
991             Criteria.checkThat(textNodeResults[0], Matchers.not(textNodeResults[1]));
992         });
993 
994         // The final result should be the separate bounding box of all four characters.
995         final AccessibilityNodeInfo finalTextNode =
996                 provider.createAccessibilityNodeInfo(textNodeVirtualViewId);
997         TestThreadUtils.runOnUiThreadBlocking(new Runnable() {
998             @Override
999             public void run() {
1000                 provider.addExtraDataToAccessibilityNodeInfo(textNodeVirtualViewId, finalTextNode,
1001                         EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY, arguments);
1002             }
1003         });
1004         extras = finalTextNode.getExtras();
1005         result = (RectF[]) extras.getParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
1006         Assert.assertNotEquals(result[0], result[1]);
1007         Assert.assertNotEquals(result[0], result[2]);
1008         Assert.assertNotEquals(result[0], result[3]);
1009 
1010         // All four should have nonzero left, top, width, and height
1011         for (int i = 0; i < 4; ++i) {
1012             Assert.assertTrue(result[i].left > 0);
1013             Assert.assertTrue(result[i].top > 0);
1014             Assert.assertTrue(result[i].width() > 0);
1015             Assert.assertTrue(result[i].height() > 0);
1016         }
1017 
1018         // They should be in order.
1019         Assert.assertTrue(result[0].left < result[1].left);
1020         Assert.assertTrue(result[1].left < result[2].left);
1021         Assert.assertTrue(result[2].left < result[3].left);
1022     }
1023 
1024     /**
1025      * Ensure scrolling |AccessibilityNodeInfo| actions are not added unless the node is
1026      * specifically user scrollable, and not just programmatically scrollable.
1027      */
1028     @Test
1029     @MediumTest
1030     @MinAndroidSdkLevel(Build.VERSION_CODES.M)
1031     @TargetApi(Build.VERSION_CODES.M)
1032     public void testAccessibilityNodeInfo_Actions_OverflowHidden() throws Throwable {
1033         // Build a simple web page with a div and overflow:hidden
1034         String htmlContent =
1035                 "<div title=\"1234\" style=\"overflow:hidden; width: 200px; height: 50px\">\n"
1036                 + "  <p>Example Paragraph 1</p>\n"
1037                 + "  <p>Example Paragraph 2</p>\n"
1038                 + "</div>";
1039 
1040         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(htmlContent));
1041         mActivityTestRule.waitForActiveShellToBeDoneLoading();
1042         mNodeProvider = enableAccessibilityAndWaitForNodeProvider();
1043 
1044         // Define our root node and paragraph node IDs by looking for their text.
1045         int virtualViewIdDiv = waitForNodeWithTextOnly(mNodeProvider, "1234");
1046         int virtualViewIdP1 = waitForNodeWithTextOnly(mNodeProvider, "Example Paragraph 1");
1047         int virtualViewIdP2 = waitForNodeWithTextOnly(mNodeProvider, "Example Paragraph 2");
1048 
1049         // Get the |AccessibilityNodeInfo| objects for our nodes.
1050         AccessibilityNodeInfo nodeInfoDiv =
1051                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdDiv);
1052         AccessibilityNodeInfo nodeInfoP1 =
1053                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdP1);
1054         AccessibilityNodeInfo nodeInfoP2 =
1055                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdP2);
1056 
1057         // Assert we have the correct nodes.
1058         Assert.assertNotEquals(nodeInfoDiv, null);
1059         Assert.assertNotEquals(nodeInfoP1, null);
1060         Assert.assertNotEquals(nodeInfoP2, null);
1061         Assert.assertEquals(nodeInfoDiv.getText().toString(), "1234");
1062         Assert.assertEquals(nodeInfoP1.getText().toString(), "Example Paragraph 1");
1063         Assert.assertEquals(nodeInfoP2.getText().toString(), "Example Paragraph 2");
1064 
1065         // Assert the scroll actions are not present in any of the objects.
1066         assertActionsContainNoScrolls(nodeInfoDiv);
1067         assertActionsContainNoScrolls(nodeInfoP1);
1068         assertActionsContainNoScrolls(nodeInfoP2);
1069 
1070         // Traverse to the next node, then re-assert.
1071         performActionOnUiThread(
1072                 virtualViewIdDiv, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, new Bundle());
1073         assertActionsContainNoScrolls(nodeInfoDiv);
1074         assertActionsContainNoScrolls(nodeInfoP1);
1075         assertActionsContainNoScrolls(nodeInfoP2);
1076 
1077         // Repeat.
1078         performActionOnUiThread(
1079                 virtualViewIdP1, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, new Bundle());
1080         assertActionsContainNoScrolls(nodeInfoDiv);
1081         assertActionsContainNoScrolls(nodeInfoP1);
1082         assertActionsContainNoScrolls(nodeInfoP2);
1083 
1084         tearDown();
1085     }
1086 
1087     /**
1088      * Ensure scrolling |AccessibilityNodeInfo| actions are added unless if the node is user
1089      * scrollable.
1090      */
1091     @Test
1092     @MediumTest
1093     @MinAndroidSdkLevel(Build.VERSION_CODES.M)
1094     @TargetApi(Build.VERSION_CODES.M)
1095     public void testAccessibilityNodeInfo_Actions_OverflowScroll() throws Throwable {
1096         // Build a simple web page with a div and overflow:scroll
1097         String htmlContent =
1098                 "<div title=\"1234\" style=\"overflow:scroll; width: 200px; height: 50px\">\n"
1099                 + "  <p>Example Paragraph 1</p>\n"
1100                 + "  <p>Example Paragraph 2</p>\n"
1101                 + "</div>";
1102 
1103         mActivityTestRule.launchContentShellWithUrl(UrlUtils.encodeHtmlDataUri(htmlContent));
1104         mActivityTestRule.waitForActiveShellToBeDoneLoading();
1105         mNodeProvider = enableAccessibilityAndWaitForNodeProvider();
1106 
1107         // Define our root node and paragraph node IDs by looking for their text.
1108         int virtualViewIdDiv = waitForNodeWithTextOnly(mNodeProvider, "1234");
1109         int virtualViewIdP1 = waitForNodeWithTextOnly(mNodeProvider, "Example Paragraph 1");
1110         int virtualViewIdP2 = waitForNodeWithTextOnly(mNodeProvider, "Example Paragraph 2");
1111 
1112         // Get the |AccessibilityNodeInfo| objects for our nodes.
1113         AccessibilityNodeInfo nodeInfoDiv =
1114                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdDiv);
1115         AccessibilityNodeInfo nodeInfoP1 =
1116                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdP1);
1117         AccessibilityNodeInfo nodeInfoP2 =
1118                 mNodeProvider.createAccessibilityNodeInfo(virtualViewIdP2);
1119 
1120         // Assert we have the correct nodes.
1121         Assert.assertNotEquals(nodeInfoDiv, null);
1122         Assert.assertNotEquals(nodeInfoP1, null);
1123         Assert.assertNotEquals(nodeInfoP2, null);
1124         Assert.assertEquals(nodeInfoDiv.getText().toString(), "1234");
1125         Assert.assertEquals(nodeInfoP1.getText().toString(), "Example Paragraph 1");
1126         Assert.assertEquals(nodeInfoP2.getText().toString(), "Example Paragraph 2");
1127 
1128         // Assert the scroll actions ARE present for our div node, but not the others.
1129         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1130                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD));
1131         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1132                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN));
1133         assertActionsContainNoScrolls(nodeInfoP1);
1134         assertActionsContainNoScrolls(nodeInfoP2);
1135 
1136         // Traverse to the next node, then re-assert.
1137         performActionOnUiThread(
1138                 virtualViewIdDiv, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, new Bundle());
1139         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1140                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD));
1141         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1142                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN));
1143         assertActionsContainNoScrolls(nodeInfoP1);
1144         assertActionsContainNoScrolls(nodeInfoP2);
1145 
1146         // Repeat.
1147         performActionOnUiThread(
1148                 virtualViewIdP1, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, new Bundle());
1149         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1150                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD));
1151         Assert.assertTrue(nodeInfoDiv.getActionList().contains(
1152                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN));
1153         assertActionsContainNoScrolls(nodeInfoP1);
1154         assertActionsContainNoScrolls(nodeInfoP2);
1155 
1156         tearDown();
1157     }
1158 
1159     @MinAndroidSdkLevel(Build.VERSION_CODES.M)
1160     @TargetApi(Build.VERSION_CODES.M)
1161     private void assertActionsContainNoScrolls(AccessibilityNodeInfo nodeInfo) {
1162         Assert.assertFalse(nodeInfo.getActionList().contains(
1163                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD));
1164         Assert.assertFalse(nodeInfo.getActionList().contains(
1165                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD));
1166         Assert.assertFalse(nodeInfo.getActionList().contains(
1167                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP));
1168         Assert.assertFalse(nodeInfo.getActionList().contains(
1169                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN));
1170         Assert.assertFalse(nodeInfo.getActionList().contains(
1171                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_LEFT));
1172         Assert.assertFalse(nodeInfo.getActionList().contains(
1173                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_RIGHT));
1174     }
1175 }
1176