1package com.xtremelabs.robolectric.shadows; 2 3import static com.xtremelabs.robolectric.Robolectric.shadowOf; 4import static java.util.Arrays.asList; 5import static org.hamcrest.CoreMatchers.equalTo; 6import static org.hamcrest.CoreMatchers.is; 7import static org.hamcrest.CoreMatchers.notNullValue; 8import static org.hamcrest.CoreMatchers.nullValue; 9import static org.hamcrest.CoreMatchers.sameInstance; 10import static org.junit.Assert.assertFalse; 11import static org.junit.Assert.assertNull; 12import static org.junit.Assert.assertThat; 13import static org.junit.Assert.fail; 14 15import java.util.ArrayList; 16import java.util.Arrays; 17import java.util.List; 18import java.util.Random; 19 20import org.junit.Before; 21import org.junit.Test; 22import org.junit.runner.RunWith; 23 24import android.util.SparseBooleanArray; 25import android.view.View; 26import android.view.ViewGroup; 27import android.widget.AdapterView; 28import android.widget.ArrayAdapter; 29import android.widget.BaseAdapter; 30import android.widget.LinearLayout; 31import android.widget.ListView; 32 33import com.xtremelabs.robolectric.WithTestDefaultsRunner; 34import com.xtremelabs.robolectric.util.Transcript; 35 36@RunWith(WithTestDefaultsRunner.class) 37public class ListViewTest { 38 39 private Transcript transcript; 40 private ListView listView; 41 private int checkedItemPosition; 42 private SparseBooleanArray checkedItemPositions; 43 private int lastCheckedPosition; 44 45 @Before 46 public void setUp() throws Exception { 47 transcript = new Transcript(); 48 listView = new ListView(null); 49 } 50 51 @Test 52 public void testSetSelection_ShouldFireOnItemSelectedListener() throws Exception { 53 listView.setAdapter(new CountingAdapter(1)); 54 ShadowHandler.idleMainLooper(); 55 56 listView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 57 @Override 58 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 59 transcript.add("item was selected: " + position); 60 } 61 62 @Override 63 public void onNothingSelected(AdapterView<?> parent) { 64 } 65 }); 66 67 listView.setSelection(0); 68 ShadowHandler.idleMainLooper(); 69 transcript.assertEventsSoFar("item was selected: 0"); 70 } 71 72 @Test 73 public void addHeaderView_ShouldThrowIfAdapterIsAlreadySet() throws Exception { 74 listView.setAdapter(new CountingAdapter(1)); 75 try { 76 listView.addHeaderView(new View(null)); 77 fail(); 78 } catch (java.lang.IllegalStateException exception) { 79 assertThat(exception.getMessage(), equalTo("Cannot add header view to list -- setAdapter has already been called")); 80 } 81 82 try { 83 listView.addHeaderView(new View(null), null, false); 84 fail(); 85 } catch (java.lang.IllegalStateException exception) { 86 assertThat(exception.getMessage(), equalTo("Cannot add header view to list -- setAdapter has already been called")); 87 } 88 } 89 90 @Test 91 public void addHeaderView_ShouldRecordHeaders() throws Exception { 92 View view0 = new View(null); 93 view0.setId(0); 94 View view1 = new View(null); 95 view1.setId(1); 96 View view2 = new View(null); 97 view2.setId(2); 98 View view3 = new View(null); 99 view3.setId(3); 100 listView.addHeaderView(view0); 101 listView.addHeaderView(view1); 102 listView.addHeaderView(view2, null, false); 103 listView.addHeaderView(view3, null, false); 104 assertThat(listView.getHeaderViewsCount(), equalTo(4)); 105 assertThat(shadowOf(listView).getHeaderViews().get(0), sameInstance(view0)); 106 assertThat(shadowOf(listView).getHeaderViews().get(1), sameInstance(view1)); 107 assertThat(shadowOf(listView).getHeaderViews().get(2), sameInstance(view2)); 108 assertThat(shadowOf(listView).getHeaderViews().get(3), sameInstance(view3)); 109 110 assertThat(listView.findViewById(0), notNullValue()); 111 assertThat(listView.findViewById(1), notNullValue()); 112 assertThat(listView.findViewById(2), notNullValue()); 113 assertThat(listView.findViewById(3), notNullValue()); 114 } 115 116 @Test 117 public void addHeaderView_shouldAttachTheViewToTheList() throws Exception { 118 View view = new View(null); 119 view.setId(42); 120 121 listView.addHeaderView(view); 122 123 assertThat(listView.findViewById(42), is(view)); 124 } 125 126 @Test 127 public void addFooterView_ShouldThrowIfAdapterIsAlreadySet() throws Exception { 128 listView.setAdapter(new CountingAdapter(1)); 129 try { 130 listView.addFooterView(new View(null)); 131 fail(); 132 } catch (java.lang.IllegalStateException exception) { 133 assertThat(exception.getMessage(), equalTo("Cannot add footer view to list -- setAdapter has already been called")); 134 135 } 136 } 137 138 @Test 139 public void addFooterView_ShouldRecordFooters() throws Exception { 140 View view0 = new View(null); 141 View view1 = new View(null); 142 listView.addFooterView(view0); 143 listView.addFooterView(view1); 144 assertThat(shadowOf(listView).getFooterViews().get(0), sameInstance(view0)); 145 assertThat(shadowOf(listView).getFooterViews().get(1), sameInstance(view1)); 146 } 147 148 @Test 149 public void addFooterView_shouldAttachTheViewToTheList() throws Exception { 150 View view = new View(null); 151 view.setId(42); 152 153 listView.addFooterView(view); 154 155 assertThat(listView.findViewById(42), is(view)); 156 } 157 158 @Test 159 public void setAdapter_shouldNotClearHeaderOrFooterViews() throws Exception { 160 View header = new View(null); 161 listView.addHeaderView(header); 162 View footer = new View(null); 163 listView.addFooterView(footer); 164 165 prepareListWithThreeItems(); 166 167 assertThat(listView.getChildCount(), equalTo(5)); 168 assertThat(listView.getChildAt(0), is(header)); 169 assertThat(listView.getChildAt(4), is(footer)); 170 } 171 172 @Test 173 public void testGetFooterViewsCount() throws Exception { 174 listView.addHeaderView(new View(null)); 175 listView.addFooterView(new View(null)); 176 listView.addFooterView(new View(null)); 177 178 prepareListWithThreeItems(); 179 180 assertThat(listView.getFooterViewsCount(), equalTo(2)); 181 } 182 183 @Test 184 public void smoothScrollBy_shouldBeRecorded() throws Exception { 185 listView.smoothScrollBy(42, 420); 186 assertThat(shadowOf(listView).getLastSmoothScrollByDistance(), equalTo(42)); 187 assertThat(shadowOf(listView).getLastSmoothScrollByDuration(), equalTo(420)); 188 } 189 190 @Test 191 public void testPerformItemClick_ShouldFireOnItemClickListener() throws Exception { 192 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 193 @Override 194 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 195 transcript.add("item was clicked: " + position); 196 } 197 }); 198 199 listView.performItemClick(null, 0, -1); 200 transcript.assertEventsSoFar("item was clicked: 0"); 201 } 202 203 @Test 204 public void testSetSelection_WhenNoItemSelectedListenerIsSet_ShouldDoNothing() throws Exception { 205 listView.setSelection(0); 206 } 207 208 @Test 209 public void shouldHaveAdapterViewCommonBehavior() throws Exception { 210 AdapterViewBehavior.shouldActAsAdapterView(listView); 211 } 212 213 @Test 214 public void findItemContainingText_shouldFindChildByString() throws Exception { 215 ShadowListView shadowListView = prepareListWithThreeItems(); 216 View item1 = shadowListView.findItemContainingText("Item 1"); 217 assertThat(item1, sameInstance(listView.getChildAt(1))); 218 } 219 220 @Test 221 public void findItemContainingText_shouldReturnNullIfNotFound() throws Exception { 222 ShadowListView shadowListView = prepareListWithThreeItems(); 223 assertThat(shadowListView.findItemContainingText("Non-existant item"), nullValue()); 224 } 225 226 @Test 227 public void clickItemContainingText_shouldPerformItemClickOnList() throws Exception { 228 ShadowListView shadowListView = prepareListWithThreeItems(); 229 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 230 @Override 231 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 232 transcript.add("clicked on item " + position); 233 } 234 }); 235 shadowListView.clickFirstItemContainingText("Item 1"); 236 transcript.assertEventsSoFar("clicked on item 1"); 237 } 238 239 @Test 240 public void clickItemContainingText_shouldPerformItemClickOnList_arrayAdapter() throws Exception { 241 ArrayList<String> adapterFileList = new ArrayList<String>(); 242 adapterFileList.add("Item 1"); 243 adapterFileList.add("Item 2"); 244 adapterFileList.add("Item 3"); 245 final ArrayAdapter<String> adapter = new ArrayAdapter<String>(null, android.R.layout.simple_list_item_1, adapterFileList); 246 listView.setAdapter(adapter); 247 ShadowHandler.idleMainLooper(); 248 ShadowListView shadowListView = shadowOf(listView); 249 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 250 @Override 251 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 252 transcript.add("clicked on item " + adapter.getItem(position)); 253 } 254 }); 255 shadowListView.clickFirstItemContainingText("Item 3"); 256 transcript.assertEventsSoFar("clicked on item Item 3"); 257 } 258 259 @Test(expected = IllegalArgumentException.class) 260 public void clickItemContainingText_shouldThrowExceptionIfNotFound() throws Exception { 261 ShadowListView shadowListView = prepareListWithThreeItems(); 262 shadowListView.clickFirstItemContainingText("Non-existant item"); 263 } 264 265 @Test 266 public void revalidate_whenItemsHaveNotChanged_shouldWork() throws Exception { 267 prepareWithListAdapter(); 268 shadowOf(listView).checkValidity(); 269 } 270 271 @Test(expected = ArrayIndexOutOfBoundsException.class) 272 public void revalidate_removingAnItemWithoutInvalidating_shouldExplode() throws Exception { 273 ListAdapter adapter = prepareWithListAdapter(); 274 adapter.items.remove(0); 275 shadowOf(listView).checkValidity(); // should 'splode! 276 } 277 278 @Test(expected = ArrayIndexOutOfBoundsException.class) 279 public void revalidate_addingAnItemWithoutInvalidating_shouldExplode() throws Exception { 280 ListAdapter adapter = prepareWithListAdapter(); 281 adapter.items.add("x"); 282 shadowOf(listView).checkValidity(); // should 'splode! 283 } 284 285 @Test(expected = RuntimeException.class) 286 public void revalidate_changingAnItemWithoutInvalidating_shouldExplode() throws Exception { 287 ListAdapter adapter = prepareWithListAdapter(); 288 adapter.items.remove(2); 289 adapter.items.add("x"); 290 shadowOf(listView).checkValidity(); // should 'splode! 291 } 292 293 @Test 294 public void testShouldBeAbleToTurnOffAutomaticRowUpdates() throws Exception { 295 try { 296 TranscriptAdapter adapter1 = new TranscriptAdapter(); 297 assertThat(adapter1.getCount(), equalTo(1)); 298 listView.setAdapter(adapter1); 299 transcript.assertEventsSoFar("called getView"); 300 transcript.clear(); 301 adapter1.notifyDataSetChanged(); 302 transcript.assertEventsSoFar("called getView"); 303 304 transcript.clear(); 305 ShadowAdapterView.automaticallyUpdateRowViews(false); 306 307 TranscriptAdapter adapter2 = new TranscriptAdapter(); 308 assertThat(adapter2.getCount(), equalTo(1)); 309 listView.setAdapter(adapter2); 310 adapter2.notifyDataSetChanged(); 311 transcript.assertNoEventsSoFar(); 312 313 } finally { 314 ShadowAdapterView.automaticallyUpdateRowViews(true); 315 } 316 } 317 318 @Test(expected = UnsupportedOperationException.class) 319 public void removeAllViews_shouldThrowAnException() throws Exception { 320 listView.removeAllViews(); 321 } 322 323 @Test(expected = UnsupportedOperationException.class) 324 public void removeView_shouldThrowAnException() throws Exception { 325 listView.removeView(new View(null)); 326 } 327 328 @Test(expected = UnsupportedOperationException.class) 329 public void removeViewAt_shouldThrowAnException() throws Exception { 330 listView.removeViewAt(0); 331 } 332 333 @Test 334 public void getPositionForView_shouldReturnThePositionInTheListForTheView() throws Exception { 335 prepareWithListAdapter(); 336 View childViewOfListItem = ((ViewGroup) listView.getChildAt(1)).getChildAt(0); 337 assertThat(listView.getPositionForView(childViewOfListItem), equalTo(1)); 338 } 339 340 @Test 341 public void getPositionForView_shouldReturnInvalidPostionForViewThatIsNotFound() throws Exception { 342 prepareWithListAdapter(); 343 assertThat(listView.getPositionForView(new View(null)), equalTo(AdapterView.INVALID_POSITION)); 344 } 345 346 @Test 347 public void revalidate_withALazyAdapterShouldWork() { 348 ListAdapter lazyAdapter = new ListAdapter() { 349 List<String> lazyItems = Arrays.asList("a", "b", "c"); 350 351 @Override 352 public View getView(int position, View convertView, ViewGroup parent) { 353 if (items.isEmpty()) items.addAll(lazyItems); 354 return super.getView(position, convertView, parent); 355 } 356 357 @Override 358 public int getCount() { 359 return lazyItems.size(); 360 } 361 }; 362 listView.setAdapter(lazyAdapter); 363 ShadowHandler.idleMainLooper(); 364 shadowOf(listView).checkValidity(); 365 } 366 367 @Test 368 public void shouldRecordLatestCallToSmoothScrollToPostion() throws Exception { 369 listView.smoothScrollToPosition(10); 370 assertThat(shadowOf(listView).getSmoothScrolledPosition(), equalTo(10)); 371 } 372 373 @Test 374 public void givenChoiceModeIsSingle_whenGettingCheckedItemPosition_thenReturnPosition() { 375 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE).withAnyItemChecked(); 376 377 assertThat(listView.getCheckedItemPosition(), is(checkedItemPosition)); 378 } 379 380 @Test 381 public void givenChoiceModeIsMultiple_whenGettingCheckedItemPosition_thenReturnInvalidPosition() { 382 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_MULTIPLE).withAnyItemChecked(); 383 384 assertThat(listView.getCheckedItemPosition(), is(ListView.INVALID_POSITION)); 385 } 386 387 @Test 388 public void givenChoiceModeIsNone_whenGettingCheckedItemPosition_thenReturnInvalidPosition() { 389 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_NONE); 390 391 assertThat(listView.getCheckedItemPosition(), is(ListView.INVALID_POSITION)); 392 } 393 394 @Test 395 public void givenNoItemsChecked_whenGettingCheckedItemOisition_thenReturnInvalidPosition() { 396 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE); 397 398 assertThat(listView.getCheckedItemPosition(), is(ListView.INVALID_POSITION)); 399 } 400 401 @Test 402 public void givenChoiceModeIsSingleAndAnItemIsChecked_whenSettingChoiceModeToNone_thenGetCheckedItemPositionShouldReturnInvalidPosition() { 403 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE).withAnyItemChecked(); 404 405 listView.setChoiceMode(ListView.CHOICE_MODE_NONE); 406 407 assertThat(listView.getCheckedItemPosition(), is(ListView.INVALID_POSITION)); 408 } 409 410 @Test 411 public void givenChoiceModeIsMultipleAndMultipleItemsAreChecked_whenGettingCheckedItemPositions_thenReturnCheckedPositions() { 412 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_MULTIPLE).withAnyItemsChecked(); 413 414 assertThat(listView.getCheckedItemPositions(), equalTo(checkedItemPositions)); 415 } 416 417 @Test 418 public void givenChoiceModeIsSingleAndMultipleItemsAreChecked_whenGettingCheckedItemPositions_thenReturnOnlyTheLastCheckedPosition() { 419 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE).withAnyItemsChecked(); 420 SparseBooleanArray expectedCheckedItemPositions = new SparseBooleanArray(); 421 expectedCheckedItemPositions.put(lastCheckedPosition, true); 422 423 assertThat(listView.getCheckedItemPositions(), equalTo(expectedCheckedItemPositions)); 424 } 425 426 @Test 427 public void givenChoiceModeIsNoneAndMultipleItemsAreChecked_whenGettingCheckedItemPositions_thenReturnNull() { 428 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_NONE).withAnyItemsChecked(); 429 430 assertNull(listView.getCheckedItemPositions()); 431 } 432 433 @Test 434 public void givenItemIsNotCheckedAndChoiceModeIsSingle_whenPerformingItemClick_thenItemShouldBeChecked() { 435 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE); 436 int positionToClick = anyListIndex(); 437 438 listView.performItemClick(null, positionToClick, 0); 439 440 assertThat(listView.getCheckedItemPosition(), equalTo(positionToClick)); 441 } 442 443 @Test 444 public void givenItemIsCheckedAndChoiceModeIsSingle_whenPerformingItemClick_thenItemShouldBeChecked() { 445 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_SINGLE).withAnyItemChecked(); 446 447 listView.performItemClick(null, checkedItemPosition, 0); 448 449 assertThat(listView.getCheckedItemPosition(), equalTo(checkedItemPosition)); 450 } 451 452 @Test 453 public void givenItemIsNotCheckedAndChoiceModeIsMultiple_whenPerformingItemClick_thenItemShouldBeChecked() { 454 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 455 int positionToClick = anyListIndex(); 456 SparseBooleanArray expectedCheckedItemPositions = new SparseBooleanArray(); 457 expectedCheckedItemPositions.put(positionToClick, true); 458 459 listView.performItemClick(null, positionToClick, 0); 460 461 assertThat(listView.getCheckedItemPositions(), equalTo(expectedCheckedItemPositions)); 462 } 463 464 @Test 465 public void givenItemIsCheckedAndChoiceModeIsMultiple_whenPerformingItemClick_thenItemShouldNotBeChecked() { 466 prepareListAdapter().withChoiceMode(ListView.CHOICE_MODE_MULTIPLE).withAnyItemChecked(); 467 468 listView.performItemClick(null, checkedItemPosition, 0); 469 470 assertFalse(listView.getCheckedItemPositions().get(checkedItemPosition)); 471 } 472 473 private ListAdapterBuilder prepareListAdapter() { 474 return new ListAdapterBuilder(); 475 } 476 477 private ListAdapter prepareWithListAdapter() { 478 ListAdapter adapter = new ListAdapter("a", "b", "c"); 479 listView.setAdapter(adapter); 480 ShadowHandler.idleMainLooper(); 481 return adapter; 482 } 483 484 private ShadowListView prepareListWithThreeItems() { 485 listView.setAdapter(new CountingAdapter(3)); 486 ShadowHandler.idleMainLooper(); 487 488 return shadowOf(listView); 489 } 490 491 private int anyListIndex() { 492 return new Random().nextInt(3); 493 } 494 495 private static class ListAdapter extends BaseAdapter { 496 public List<String> items = new ArrayList<String>(); 497 498 public ListAdapter(String... items) { 499 this.items.addAll(asList(items)); 500 } 501 502 @Override 503 public int getCount() { 504 return items.size(); 505 } 506 507 @Override 508 public Object getItem(int position) { 509 return items.get(position); 510 } 511 512 @Override 513 public long getItemId(int position) { 514 return 0; 515 } 516 517 @Override 518 public View getView(int position, View convertView, ViewGroup parent) { 519 LinearLayout linearLayout = new LinearLayout(null); 520 linearLayout.addView(new View(null)); 521 return linearLayout; 522 } 523 } 524 525 public class ListAdapterBuilder { 526 527 public ListAdapterBuilder() { 528 prepareListWithThreeItems(); 529 } 530 531 public ListAdapterBuilder withChoiceMode(int choiceMode) { 532 listView.setChoiceMode(choiceMode); 533 return this; 534 } 535 536 public ListAdapterBuilder withAnyItemChecked() { 537 checkedItemPosition = anyListIndex(); 538 listView.setItemChecked(checkedItemPosition, true); 539 return this; 540 } 541 542 public void withAnyItemsChecked() { 543 checkedItemPositions = new SparseBooleanArray(); 544 int numberOfSelections = anyListIndex() + 1; 545 for (int i = 0; i < numberOfSelections; i++) { 546 checkedItemPositions.put(i, true); 547 listView.setItemChecked(i, true); 548 lastCheckedPosition = i; 549 } 550 551 } 552 } 553 554 private class TranscriptAdapter extends BaseAdapter { 555 @Override 556 public int getCount() { 557 return 1; 558 } 559 560 @Override 561 public Object getItem(int position) { 562 return null; 563 } 564 565 @Override 566 public long getItemId(int position) { 567 return position; 568 } 569 570 @Override 571 public View getView(int position, View convertView, ViewGroup parent) { 572 transcript.add("called getView"); 573 return new View(parent.getContext()); 574 } 575 } 576} 577