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