/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.talkback;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
import android.widget.ListView;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import com.android.switchaccess.test.ShadowAccessibilityNodeInfo;
import com.android.talkback.CollectionState;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.Role;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.internal.ShadowExtractor;
import java.lang.CharSequence;
import java.util.ArrayList;
import java.util.List;
@Config(constants = BuildConfig.class,
sdk = 21,
shadows = {ShadowAccessibilityNodeInfo.class})
@RunWith(RobolectricGradleTestRunner.class)
public class CollectionStateTest {
private CollectionState mCollectionState;
private Context mContext = RuntimeEnvironment.application.getApplicationContext();
private static final String TEST_LIST_NAME_1 = "Foobar";
private static final String TEST_LIST_NAME_2 = "Foobaz";
private static final String TEST_LIST_NAME_3 = "Barbaz";
private static final String TEST_GRID_NAME = "Bazbar";
/** Make the root a WebView because this triggers headers to work on M or lower. */
private static final String WEBVIEW_CLASS_NAME = "android.webkit.WebView";
private static final String RECYCLERVIEW_CLASS_NAME = "android.support.v7.widget.RecyclerView";
@Before
public void setUp() {
mCollectionState = new CollectionState();
}
@After
public void tearDown() {
assertFalse(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(true));
ShadowAccessibilityNodeInfo.resetObtainedInstances();
}
@Test
public void testList() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> items = new ArrayList<>();
AccessibilityNodeInfo list = createList(TEST_LIST_NAME_1, 10, 2, items);
getShadow(root).addChild(list);
try {
// Outside of list -> Outside of list
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_NONE, mCollectionState.getCollectionTransition());
// Outside of list -> Item 1
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(items.get(1));
mCollectionState.updateCollectionInformation(item1, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
1 /* itemIndex */,
10 /* totalItems */,
false /* isItemHeader */);
// Item 1 -> Item 2 (header)
AccessibilityNodeInfoCompat item2 = new AccessibilityNodeInfoCompat(items.get(2));
mCollectionState.updateCollectionInformation(item2, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
2 /* itemIndex */,
10 /* totalItems */,
true /* isItemHeader */);
// Item 2 (header) -> Refocus
mCollectionState.updateCollectionInformation(item2, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged (must be true on refocus) */,
2 /* itemIndex */,
10 /* totalItems */,
true /* isItemHeader */);
// Item 2 -> Outside of list
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
// Outside of list -> Item 4
AccessibilityNodeInfoCompat item4 = new AccessibilityNodeInfoCompat(items.get(4));
mCollectionState.updateCollectionInformation(item4, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged (must be true on refocus) */,
4 /* itemIndex */,
10 /* totalItems */,
false /* isItemHeader */);
// Item 4 -> Outside of list
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
// Outside of list -> Refocus
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_NONE, mCollectionState.getCollectionTransition());
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(items);
}
}
@Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
@Test
public void testList_JellyBean() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> items = new ArrayList<>();
List<AccessibilityEvent> events = new ArrayList<>();
AccessibilityNodeInfo list = createListWithJellyBeanEvents(TEST_LIST_NAME_1, 10, 2, items,
events);
getShadow(root).addChild(list);
try {
// Outside of list -> Outside of list
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_NONE, mCollectionState.getCollectionTransition());
// Outside of list -> Item 1
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(items.get(1));
mCollectionState.updateCollectionInformation(item1, events.get(1));
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 1 -> Item 2 (header)
AccessibilityNodeInfoCompat item2 = new AccessibilityNodeInfoCompat(items.get(2));
mCollectionState.updateCollectionInformation(item2, events.get(2));
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
2 /* itemIndex */,
-1 /* totalItems */,
true /* isItemHeader */);
// Item 2 (header) -> Refocus
mCollectionState.updateCollectionInformation(item2, events.get(2));
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged (must be true on refocus) */,
2 /* itemIndex */,
-1 /* totalItems */,
true /* isItemHeader */);
// Item 2 -> Outside of list
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
// Outside of list -> Item 4
AccessibilityNodeInfoCompat item4 = new AccessibilityNodeInfoCompat(items.get(4));
mCollectionState.updateCollectionInformation(item4, events.get(4));
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged (must be true on refocus) */,
4 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 4 -> Outside of list
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
// Outside of list -> Refocus
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_NONE, mCollectionState.getCollectionTransition());
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(items);
for (AccessibilityEvent event : events) {
event.recycle();
}
}
}
@Test
public void testTwoLists() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> itemsA = new ArrayList<>();
AccessibilityNodeInfo listA = createList(TEST_LIST_NAME_1, 8, 3, itemsA);
getShadow(root).addChild(listA);
List<AccessibilityNodeInfo> itemsB = new ArrayList<>();
AccessibilityNodeInfo listB = createList(TEST_LIST_NAME_2, 5, 0, itemsB);
getShadow(root).addChild(listB);
try {
// Outside of list -> Item A6
AccessibilityNodeInfoCompat itemA6 = new AccessibilityNodeInfoCompat(itemsA.get(6));
mCollectionState.updateCollectionInformation(itemA6, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
6 /* itemIndex */,
8 /* totalItems */,
false /* isItemHeader */);
// Item A6 -> Item A3
AccessibilityNodeInfoCompat itemA3 = new AccessibilityNodeInfoCompat(itemsA.get(3));
mCollectionState.updateCollectionInformation(itemA3, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
3 /* itemIndex */,
8 /* totalItems */,
true /* isItemHeader */);
// Item A3 -> B3
AccessibilityNodeInfoCompat itemB3 = new AccessibilityNodeInfoCompat(itemsB.get(3));
mCollectionState.updateCollectionInformation(itemB3, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_2 /* listName */,
true /* rowChanged */,
3 /* itemIndex */,
5 /* totalItems */,
false /* isItemHeader */);
// Item B3 -> Item B0
AccessibilityNodeInfoCompat itemB0 = new AccessibilityNodeInfoCompat(itemsB.get(0));
mCollectionState.updateCollectionInformation(itemB0, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_2 /* listName */,
true /* rowChanged */,
0 /* itemIndex */,
5 /* totalItems */,
true /* isItemHeader */);
// Item B0 -> Item B4
AccessibilityNodeInfoCompat itemB4 = new AccessibilityNodeInfoCompat(itemsB.get(4));
mCollectionState.updateCollectionInformation(itemB4, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_2 /* listName */,
true /* rowChanged */,
4 /* itemIndex */,
5 /* totalItems */,
false /* isItemHeader */);
// Item B4 -> Outside of list
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
listA.recycle();
listB.recycle();
recycleNodes(itemsA);
recycleNodes(itemsB);
}
}
@Test
public void testSectionedGrid() {
// This is the grid used in the test:
// +-----------------------+
// | Fruit (header) |
// +-----------------------+
// | (1) | (2) | (3) | (4) |
// +-----+-----+-----+-----+
// | (5) | | | |
// +-----------------------+
// | Vegetable (header) |
// +-----------------------+
// | (1) | (2) | (3) | (4) |
// +-----------------------+
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> gridItems = new ArrayList<>();
AccessibilityNodeInfo grid = createGrid(TEST_GRID_NAME, 5, 4);
getShadow(root).addChild(grid);
addGridSectionHeader("Fruit", 0, 4, grid, gridItems); // 0
addGridItem("Apple", 1, 0, false, grid, gridItems); // 1
addGridItem("Banana", 1, 1, false, grid, gridItems); // 2
addGridItem("Cherry", 1, 2, false, grid, gridItems); // 3
addGridItem("Date", 1, 3, false, grid, gridItems); // 4
addGridItem("Elderberry", 2, 0, false, grid, gridItems); // 5
addGridSectionHeader("Vegetable", 3, 4, grid, gridItems); // 6
addGridItem("Arugula", 4, 0, false, grid, gridItems); // 7
addGridItem("Bok choy", 4, 1, false, grid, gridItems); // 8
addGridItem("Cauliflower", 4, 2, false, grid, gridItems); // 9
addGridItem("Dill", 4, 3, false, grid, gridItems); // 10
try {
// Outside of list -> Item Cherry
AccessibilityNodeInfoCompat cherry = new AccessibilityNodeInfoCompat(gridItems.get(3));
mCollectionState.updateCollectionInformation(cherry, null);
assertGrid(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
true /* colChanged */,
1 /* itemRow */, 2 /* itemCol */,
null /* rowName */, null /* colName */,
CollectionState.TYPE_NONE /* headingType */);
// Item Cherry -> Header Fruit
AccessibilityNodeInfoCompat fruit = new AccessibilityNodeInfoCompat(gridItems.get(0));
mCollectionState.updateCollectionInformation(fruit, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
true /* colChanged */,
0 /* itemRow */, -1 /* itemCol */,
null /* rowName */, null /* colName */,
CollectionState.TYPE_INDETERMINATE /* headingType */);
// Header Fruit -> Header Vegetable
AccessibilityNodeInfoCompat veggie = new AccessibilityNodeInfoCompat(gridItems.get(6));
mCollectionState.updateCollectionInformation(veggie, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
false /* colChanged */,
3 /* itemRow */, -1 /* itemCol */,
null /* rowName */, null /* colName */,
CollectionState.TYPE_INDETERMINATE /* headingType */);
// Header Vegetable -> Item Arugula
AccessibilityNodeInfoCompat arugula = new AccessibilityNodeInfoCompat(gridItems.get(7));
mCollectionState.updateCollectionInformation(arugula, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
true /* colChanged */,
4 /* itemRow */, 0 /* itemCol */,
null /* rowName */, null /* colName */,
CollectionState.TYPE_NONE /* headingType */);
// Item Arugula -> Item Bok choy
AccessibilityNodeInfoCompat bokchoy = new AccessibilityNodeInfoCompat(gridItems.get(8));
mCollectionState.updateCollectionInformation(bokchoy, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
false /* rowChanged */,
true /* colChanged */,
4 /* itemRow */, 1 /* itemCol */,
null /* rowName */, null /* colName */,
CollectionState.TYPE_NONE /* headingType */);
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
grid.recycle();
recycleNodes(gridItems);
}
}
@Test
public void testDataTableGrid() {
// This is the grid used in the test:
// Name Shape Size
// ==== ===== ====
// Apple Round Medium
// Banana Cylinder Medium
// Cherry Round Small
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> gridItems = new ArrayList<>();
AccessibilityNodeInfo grid = createGrid(TEST_GRID_NAME, 4, 3);
getShadow(root).addChild(grid);
addGridItem("Name", 0, 0, true, grid, gridItems); // 0
addGridItem("Shape", 0, 1, true, grid, gridItems); // 1
addGridItem("Size", 0, 2, true, grid, gridItems); // 2
addGridItem("Apple", 1, 0, false, grid, gridItems); // 3
addGridItem("Round", 1, 1, false, grid, gridItems); // 4
addGridItem("Medium", 1, 2, false, grid, gridItems); // 5
addGridItem("Banana", 2, 0, false, grid, gridItems); // 6
addGridItem("Cylinder", 2, 1, false, grid, gridItems); // 7
addGridItem("Medium", 2, 2, false, grid, gridItems); // 8
addGridItem("Cherry", 3, 0, false, grid, gridItems); // 9
addGridItem("Round", 3, 1, false, grid, gridItems); // 10
addGridItem("Small", 3, 2, false, grid, gridItems); // 11
try {
// Outside of list -> Item Small (row 3, col 2)
AccessibilityNodeInfoCompat small = new AccessibilityNodeInfoCompat(gridItems.get(11));
mCollectionState.updateCollectionInformation(small, null);
assertGrid(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
true /* colChanged */,
3 /* itemRow */, 2 /* itemCol */,
null /* rowName */, "Size" /* colName */,
CollectionState.TYPE_NONE /* headingType */);
// Item Small (row 3, col 2) -> Item Round (row 3, col 1)
AccessibilityNodeInfoCompat roundA = new AccessibilityNodeInfoCompat(gridItems.get(10));
mCollectionState.updateCollectionInformation(roundA, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
false /* rowChanged */,
true /* colChanged */,
3 /* itemRow */, 1 /* itemCol */,
null /* rowName */, "Shape" /* colName */,
CollectionState.TYPE_NONE /* headingType */);
// Item Round (row 3, col 1) -> Item Round (row 1, col 1)
AccessibilityNodeInfoCompat roundB = new AccessibilityNodeInfoCompat(gridItems.get(4));
mCollectionState.updateCollectionInformation(roundB, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
false /* colChanged */,
1 /* itemRow */, 1 /* itemCol */,
null /* rowName */, "Shape" /* colName */,
CollectionState.TYPE_NONE /* headingType */);
// Item Round (row 1, col 1) -> Header Shape
AccessibilityNodeInfoCompat shape = new AccessibilityNodeInfoCompat(gridItems.get(1));
mCollectionState.updateCollectionInformation(shape, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
false /* colChanged */,
0 /* itemRow */, 1 /* itemCol */,
null /* rowName */, "Shape" /* colName */,
CollectionState.TYPE_COLUMN /* headingType */);
// Header Shape -> Item Apple
AccessibilityNodeInfoCompat apple = new AccessibilityNodeInfoCompat(gridItems.get(3));
mCollectionState.updateCollectionInformation(apple, null);
assertGrid(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_GRID_NAME /* gridName */,
true /* rowChanged */,
true /* colChanged */,
1 /* itemRow */, 0 /* itemCol */,
null /* rowName */, "Name" /* colName */,
CollectionState.TYPE_NONE /* headingType */);
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
grid.recycle();
recycleNodes(gridItems);
}
}
@Test
public void testIncompleteInformation() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> listItems = new ArrayList<>();
AccessibilityNodeInfo list = createList(TEST_LIST_NAME_1,
5 /* numItems */,
-1 /* headingIndex */,
listItems);
getShadow(root).addChild(list);
try {
// Set the first collection item info to null,
listItems.get(0).setCollectionItemInfo(null);
AccessibilityNodeInfoCompat item0 = new AccessibilityNodeInfoCompat(listItems.get(0));
mCollectionState.updateCollectionInformation(item0, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
false /* rowChanged */);
// Move to item with actual collection item info.
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(listItems.get(1));
mCollectionState.updateCollectionInformation(item1, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
1 /* itemIndex */,
5 /* totalItems */,
false /* isItemHeader */);
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(listItems);
}
}
@Test
public void test1x1_shouldNotEnter() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
List<AccessibilityNodeInfo> listItems = new ArrayList<>();
AccessibilityNodeInfo list = createList(TEST_LIST_NAME_1,
1 /* numItems */,
-1 /* headingIndex */,
listItems);
getShadow(root).addChild(list);
try {
AccessibilityNodeInfoCompat item0 = new AccessibilityNodeInfoCompat(listItems.get(0));
mCollectionState.updateCollectionInformation(item0, null);
assertEquals(Role.ROLE_NONE, mCollectionState.getCollectionRole());
assertEquals(CollectionState.NAVIGATE_NONE, mCollectionState.getCollectionTransition());
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(listItems);
}
}
@Test
public void testMalformedListView() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
// Create list w/o CollectionInfo.
AccessibilityNodeInfo list = AccessibilityNodeInfo.obtain();
list.setContentDescription(TEST_LIST_NAME_1);
list.setClassName(ListView.class.getName());
getShadow(root).addChild(list);
// Add list items w/o CollectionItemInfo.
List<AccessibilityNodeInfo> items = new ArrayList<>();
for (int i = 0; i < 5; ++i) {
AccessibilityNodeInfo child = AccessibilityNodeInfo.obtain();
child.setContentDescription("Item " + i);
getShadow(list).addChild(child);
items.add(child);
}
// CollectionState should recognize we're going into the ListView, but it doesn't need to
// figure out list item indices.
try {
// Move to first item.
AccessibilityNodeInfoCompat item0 = new AccessibilityNodeInfoCompat(items.get(0));
mCollectionState.updateCollectionInformation(item0, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
false /* rowChanged */);
// Move to second item.
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(items.get(1));
mCollectionState.updateCollectionInformation(item1, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
false /* rowChanged */);
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(items);
}
}
@Test
public void testWorkarounds_PreN_ListView() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
List<AccessibilityNodeInfo> items = new ArrayList<>();
AccessibilityNodeInfo list = createList(TEST_LIST_NAME_1, 10, 0, items);
list.setClassName(ListView.class.getName());
getShadow(root).addChild(list);
// Headers and item indices should be omitted inside ListViews (pre-N, no WebView ancestor).
// Rows should change between items (so we can possible announce headers), even though
// the display row remains -1.
try {
// Outside of list -> Item 0
AccessibilityNodeInfoCompat item0 = new AccessibilityNodeInfoCompat(items.get(0));
mCollectionState.updateCollectionInformation(item0, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 0 -> Item 1
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(items.get(1));
mCollectionState.updateCollectionInformation(item1, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 1 -> Item 2
AccessibilityNodeInfoCompat item2 = new AccessibilityNodeInfoCompat(items.get(2));
mCollectionState.updateCollectionInformation(item2, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 2 -> Outside of list
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(items);
}
}
@Test
public void testWorkarounds_PreN_RecyclerView() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
List<AccessibilityNodeInfo> items = new ArrayList<>();
AccessibilityNodeInfo list = createList(TEST_LIST_NAME_1, 10, 0, items);
list.setClassName(RECYCLERVIEW_CLASS_NAME);
getShadow(root).addChild(list);
// Item indices should be omitted but headers *should not* be omitted inside RecyclerViews
// (pre-N, no WebView ancestor).
// Rows should change between items (so we can possible announce headers), even though
// the display row remains -1.
try {
// Outside of list -> Item 0
AccessibilityNodeInfoCompat item0 = new AccessibilityNodeInfoCompat(items.get(0));
mCollectionState.updateCollectionInformation(item0, null);
assertList(CollectionState.NAVIGATE_ENTER /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
true /* isItemHeader */);
// Item 0 -> Item 1
AccessibilityNodeInfoCompat item1 = new AccessibilityNodeInfoCompat(items.get(1));
mCollectionState.updateCollectionInformation(item1, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 1 -> Item 2
AccessibilityNodeInfoCompat item2 = new AccessibilityNodeInfoCompat(items.get(2));
mCollectionState.updateCollectionInformation(item2, null);
assertList(CollectionState.NAVIGATE_INTERIOR /* collectionTransition */,
TEST_LIST_NAME_1 /* listName */,
true /* rowChanged */,
-1 /* itemIndex */,
-1 /* totalItems */,
false /* isItemHeader */);
// Item 2 -> Outside of list
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
mCollectionState.updateCollectionInformation(rootCompat, null);
assertEquals(CollectionState.NAVIGATE_EXIT, mCollectionState.getCollectionTransition());
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list.recycle();
recycleNodes(items);
}
}
@Test
public void testNested_mostlyHierarchical() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
AccessibilityNodeInfo list1 = AccessibilityNodeInfo.obtain();
list1.setCollectionInfo(CollectionInfo.obtain(2, 1, true /* hierarchical */));
getShadow(root).addChild(list1);
AccessibilityNodeInfo list2 = AccessibilityNodeInfo.obtain();
list2.setCollectionInfo(CollectionInfo.obtain(2, 1, true /* hierarchical */));
list2.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list1).addChild(list2);
AccessibilityNodeInfo list3 = AccessibilityNodeInfo.obtain();
list3.setCollectionInfo(CollectionInfo.obtain(2, 1, false /* hierarchical */));
list3.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list2).addChild(list3);
AccessibilityNodeInfo item = AccessibilityNodeInfo.obtain();
item.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list3).addChild(item);
try {
// list2 is inside of list1
AccessibilityNodeInfoCompat listCompat2 = new AccessibilityNodeInfoCompat(list2);
mCollectionState.updateCollectionInformation(listCompat2, null);
assertEquals(0, mCollectionState.getCollectionLevel());
// list3 is inside of list2
AccessibilityNodeInfoCompat listCompat3 = new AccessibilityNodeInfoCompat(list3);
mCollectionState.updateCollectionInformation(listCompat3, null);
assertEquals(1, mCollectionState.getCollectionLevel());
// item is inside of list3
AccessibilityNodeInfoCompat itemCompat = new AccessibilityNodeInfoCompat(item);
mCollectionState.updateCollectionInformation(itemCompat, null);
assertEquals(-1, mCollectionState.getCollectionLevel()); // list3 not hierarchical!
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list1.recycle();
list2.recycle();
list3.recycle();
item.recycle();
}
}
@Test
public void testNested_mostlyNonHierarchical() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
root.setClassName(WEBVIEW_CLASS_NAME);
AccessibilityNodeInfo list1 = AccessibilityNodeInfo.obtain();
list1.setCollectionInfo(CollectionInfo.obtain(2, 1, false /* hierarchical */));
getShadow(root).addChild(list1);
AccessibilityNodeInfo list2 = AccessibilityNodeInfo.obtain();
list2.setCollectionInfo(CollectionInfo.obtain(2, 1, false /* hierarchical */));
list2.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list1).addChild(list2);
AccessibilityNodeInfo list3 = AccessibilityNodeInfo.obtain();
list3.setCollectionInfo(CollectionInfo.obtain(2, 1, true /* hierarchical */));
list3.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list2).addChild(list3);
AccessibilityNodeInfo item = AccessibilityNodeInfo.obtain();
item.setCollectionItemInfo(CollectionItemInfo.obtain(0, 1, 0, 1, false));
getShadow(list3).addChild(item);
try {
// list2 is inside of list1
AccessibilityNodeInfoCompat listCompat2 = new AccessibilityNodeInfoCompat(list2);
mCollectionState.updateCollectionInformation(listCompat2, null);
assertEquals(-1, mCollectionState.getCollectionLevel()); // list1 not hierarchical!
// list3 is inside of list2
AccessibilityNodeInfoCompat listCompat3 = new AccessibilityNodeInfoCompat(list3);
mCollectionState.updateCollectionInformation(listCompat3, null);
assertEquals(-1, mCollectionState.getCollectionLevel()); // list2 not hierarchical!
// item is inside of list3
AccessibilityNodeInfoCompat itemCompat = new AccessibilityNodeInfoCompat(item);
mCollectionState.updateCollectionInformation(itemCompat, null);
assertEquals(0, mCollectionState.getCollectionLevel());
AccessibilityNodeInfoCompat rootCompat = new AccessibilityNodeInfoCompat(root);
forceInternalRecycle(rootCompat);
} finally {
root.recycle();
list1.recycle();
list2.recycle();
list3.recycle();
item.recycle();
}
}
@Test(timeout=100)
public void testGetHeaderText_withLoop() {
AccessibilityNodeInfo a = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo b = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo c = AccessibilityNodeInfo.obtain();
getShadow(a).addChild(b);
getShadow(b).addChild(c);
getShadow(c).addChild(a);
assertNull(CollectionState.getHeaderText(new AccessibilityNodeInfoCompat(a)));
a.recycle();
b.recycle();
c.recycle();
}
@Test
public void testGetHeaderText_singleChildren() {
AccessibilityNodeInfo a = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo b = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo c = AccessibilityNodeInfo.obtain();
c.setText("Header");
getShadow(a).addChild(b);
getShadow(b).addChild(c);
assertEquals("Header", CollectionState.getHeaderText(new AccessibilityNodeInfoCompat(a)));
a.recycle();
b.recycle();
c.recycle();
}
@Test
public void testGetHeaderText_multipleChildren() {
AccessibilityNodeInfo a = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo b = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo c = AccessibilityNodeInfo.obtain();
AccessibilityNodeInfo d = AccessibilityNodeInfo.obtain();
d.setText("Header");
getShadow(a).addChild(b);
getShadow(b).addChild(c);
getShadow(b).addChild(d);
assertNull(CollectionState.getHeaderText(new AccessibilityNodeInfoCompat(a)));
a.recycle();
b.recycle();
c.recycle();
d.recycle();
}
private AccessibilityNodeInfo createList(String name,
int numItems,
int headingIndex,
List<AccessibilityNodeInfo> items) {
AccessibilityNodeInfo list = AccessibilityNodeInfo.obtain();
list.setContentDescription(name);
list.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(numItems, 1, false));
ShadowAccessibilityNodeInfo shadow = getShadow(list);
for (int i = 0; i < numItems; ++i) {
AccessibilityNodeInfo item = AccessibilityNodeInfo.obtain();
item.setContentDescription("Item " + i);
item.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(i, 1, 1, 1,
i == headingIndex));
shadow.addChild(item);
items.add(item);
}
return list;
}
private AccessibilityNodeInfo createListWithJellyBeanEvents(String name,
int numItems,
int headingIndex,
List<AccessibilityNodeInfo> items,
List<AccessibilityEvent> events) {
AccessibilityNodeInfo list = AccessibilityNodeInfo.obtain();
list.setContentDescription(name);
list.setClassName(ListView.class.getName());
ShadowAccessibilityNodeInfo shadow = getShadow(list);
for (int i = 0; i < numItems; ++i) {
AccessibilityNodeInfo item = AccessibilityNodeInfo.obtain();
item.setContentDescription("Item " + i);
shadow.addChild(item);
items.add(item);
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setAction(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
Bundle parcelable = new Bundle();
parcelable.putInt(CollectionState.EVENT_ROW, i);
parcelable.putInt(CollectionState.EVENT_COLUMN, 1);
parcelable.putBoolean(CollectionState.EVENT_HEADING, headingIndex == i);
event.setParcelableData(parcelable);
events.add(event);
}
return list;
}
private AccessibilityNodeInfo createGrid(String name, int rows, int cols) {
AccessibilityNodeInfo grid = AccessibilityNodeInfo.obtain();
grid.setContentDescription(name);
grid.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(rows, cols, false));
return grid;
}
private void addGridSectionHeader(String name, int row, int colSpan,
AccessibilityNodeInfo grid,
List<AccessibilityNodeInfo> items) {
AccessibilityNodeInfo header = AccessibilityNodeInfo.obtain();
header.setContentDescription(name);
header.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(row, 1, 1,
colSpan, true));
ShadowAccessibilityNodeInfo gridShadow = getShadow(grid);
gridShadow.addChild(header);
items.add(header);
}
private void addGridItem(String name, int row, int col, boolean header,
AccessibilityNodeInfo grid,
List<AccessibilityNodeInfo> items) {
AccessibilityNodeInfo item = AccessibilityNodeInfo.obtain();
item.setContentDescription(name);
item.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(row, 1, col,
1, header));
ShadowAccessibilityNodeInfo gridShadow = getShadow(grid);
gridShadow.addChild(item);
items.add(item);
}
private void assertList(@CollectionState.CollectionTransition int collectionTransition,
CharSequence listName,
boolean rowChanged) {
assertEquals(Role.ROLE_LIST, mCollectionState.getCollectionRole());
assertEquals(collectionTransition, mCollectionState.getCollectionTransition());
assertCharSequenceEquals(listName, mCollectionState.getCollectionName());
assertEquals(rowChanged,
(mCollectionState.getRowColumnTransition() & CollectionState.TYPE_ROW) != 0);
}
private void assertList(@CollectionState.CollectionTransition int collectionTransition,
CharSequence listName,
boolean rowChanged,
int itemIndex,
int totalItems,
boolean isItemHeader) {
assertList(collectionTransition, listName, rowChanged);
CollectionState.ListItemState itemState = mCollectionState.getListItemState();
assertEquals(itemIndex, itemState.getIndex());
assertEquals(totalItems, mCollectionState.getCollectionRowCount());
assertEquals(isItemHeader, itemState.isHeading());
}
private void assertGrid(@CollectionState.CollectionTransition int collectionTransition,
CharSequence gridName,
boolean rowChanged,
boolean colChanged,
int itemRow,
int itemCol,
CharSequence rowName,
CharSequence colName,
@CollectionState.TableHeadingType int headingType) {
assertEquals(Role.ROLE_GRID, mCollectionState.getCollectionRole());
assertEquals(collectionTransition, mCollectionState.getCollectionTransition());
assertCharSequenceEquals(gridName, mCollectionState.getCollectionName());
assertEquals(rowChanged,
(mCollectionState.getRowColumnTransition() & CollectionState.TYPE_ROW) != 0);
assertEquals(colChanged,
(mCollectionState.getRowColumnTransition() & CollectionState.TYPE_COLUMN) != 0);
CollectionState.TableItemState itemState = mCollectionState.getTableItemState();
assertEquals(itemRow, itemState.getRowIndex());
assertEquals(itemCol, itemState.getColumnIndex());
assertCharSequenceEquals(rowName, itemState.getRowName());
assertCharSequenceEquals(colName, itemState.getColumnName());
assertEquals(headingType, itemState.getHeadingType());
}
private void assertCharSequenceEquals(CharSequence a, CharSequence b) {
if (a == null) {
assertNull(b);
} else if (b == null) {
assertNull(a);
} else {
assertTrue(a.toString().equals(b.toString()));
}
}
private void assertNotEquals(Object a, Object b) {
if (a == null) {
assertNotNull(b);
} else {
assertFalse(a.equals(b));
}
}
private ShadowAccessibilityNodeInfo getShadow(AccessibilityNodeInfo info) {
return (ShadowAccessibilityNodeInfo) ShadowExtractor.extract(info);
}
private void recycleNodes(List<AccessibilityNodeInfo> nodes) {
for (AccessibilityNodeInfo node : nodes) {
node.recycle();
}
}
/**
* Forces the CollectionState into NAVIGATE_NONE state, which will cause all nodes to be
* recycled. In all tests, the root node is outside of a collection.
*/
private void forceInternalRecycle(AccessibilityNodeInfoCompat rootNode) {
// Forces transition to NAVIGATE_EXIT because we move outside of any collection.
mCollectionState.updateCollectionInformation(rootNode, null);
// Forces transition to NAVIGATE_NONE on the second movement outside of any collection.
mCollectionState.updateCollectionInformation(rootNode, null);
}
}