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