/* * Copyright 2017 The Android Open Source Project * * 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 androidx.car.widget; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.number.IsCloseTo.closeTo; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import android.graphics.drawable.Drawable; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; import android.support.test.filters.SmallTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.support.v7.widget.LinearLayoutManager; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import androidx.car.test.R; /** * Tests the layout configuration in {@link TextListItem}. */ @RunWith(AndroidJUnit4.class) @SmallTest public class TextListItemTest { @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(PagedListViewTestActivity.class); private PagedListViewTestActivity mActivity; private PagedListView mPagedListView; @Before public void setUp() { mActivity = mActivityRule.getActivity(); mPagedListView = mActivity.findViewById(R.id.paged_list_view); } private void setupPagedListView(List items) { ListItemProvider provider = new ListItemProvider.ListProvider( new ArrayList<>(items)); try { mActivityRule.runOnUiThread(() -> { mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider)); }); } catch (Throwable throwable) { throwable.printStackTrace(); throw new RuntimeException(throwable); } // Wait for paged list view to layout by using espresso to scroll to a position. onView(withId(R.id.recycler_view)).perform(scrollToPosition(0)); } private static void verifyViewIsHidden(View view) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; final int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; i++) { verifyViewIsHidden(viewGroup.getChildAt(i)); } } else { assertThat(view.getVisibility(), is(equalTo(View.GONE))); } } private TextListItem.ViewHolder getViewHolderAtPosition(int position) { return (TextListItem.ViewHolder) mPagedListView.getRecyclerView() .findViewHolderForAdapterPosition( position); } @Test public void testEmptyItemHidesAllViews() { TextListItem item = new TextListItem(mActivity); setupPagedListView(Arrays.asList(item)); verifyViewIsHidden(mPagedListView.getRecyclerView().getLayoutManager().getChildAt(0)); } @Test public void testPrimaryActionVisible() { TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, true); TextListItem item1 = new TextListItem(mActivity); item1.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); List items = Arrays.asList(item0, item1); setupPagedListView(items); assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(getViewHolderAtPosition(1).getPrimaryIcon().getVisibility(), is(equalTo(View.VISIBLE))); } @Test public void testTextVisible() { TextListItem item0 = new TextListItem(mActivity); item0.setTitle("title"); TextListItem item1 = new TextListItem(mActivity); item1.setBody("body"); List items = Arrays.asList(item0, item1); setupPagedListView(items); assertThat(getViewHolderAtPosition(0).getTitle().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(getViewHolderAtPosition(1).getBody().getVisibility(), is(equalTo(View.VISIBLE))); } @Test public void testSupplementalActionVisible() { TextListItem item0 = new TextListItem(mActivity); item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, true); TextListItem item1 = new TextListItem(mActivity); item1.setAction("text", true, v -> { /* Do nothing. */ }); TextListItem item2 = new TextListItem(mActivity); item2.setActions("text", true, v -> { /* Do nothing. */ }, "text", true, v -> { /* Do nothing. */ }); List items = Arrays.asList(item0, item1, item2); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getSupplementalIcon().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getSupplementalIconDivider().getVisibility(), is(equalTo(View.VISIBLE))); viewHolder = getViewHolderAtPosition(1); assertThat(viewHolder.getAction1().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction1Divider().getVisibility(), is(equalTo(View.VISIBLE))); viewHolder = getViewHolderAtPosition(2); assertThat(viewHolder.getAction1().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction1Divider().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction2().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction2Divider().getVisibility(), is(equalTo(View.VISIBLE))); } @Test public void testSwitchVisibleAndCheckedState() { TextListItem item0 = new TextListItem(mActivity); item0.setSwitch(true, true, null); TextListItem item1 = new TextListItem(mActivity); item1.setSwitch(false, true, null); List items = Arrays.asList(item0, item1); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(true))); assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.VISIBLE))); viewHolder = getViewHolderAtPosition(1); assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(false))); assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.VISIBLE))); } @Test public void testDividersAreOptional() { TextListItem item0 = new TextListItem(mActivity); item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, false); TextListItem item1 = new TextListItem(mActivity); item1.setAction("text", false, v -> { /* Do nothing. */ }); TextListItem item2 = new TextListItem(mActivity); item2.setActions("text", false, v -> { /* Do nothing. */ }, "text", false, v -> { /* Do nothing. */ }); TextListItem item3 = new TextListItem(mActivity); item3.setSwitch(true, false, null); List items = Arrays.asList(item0, item1, item2, item3); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getSupplementalIcon().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getSupplementalIconDivider().getVisibility(), is(equalTo(View.GONE))); viewHolder = getViewHolderAtPosition(1); assertThat(viewHolder.getAction1().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction1Divider().getVisibility(), is(equalTo(View.GONE))); viewHolder = getViewHolderAtPosition(2); assertThat(viewHolder.getAction1().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction1Divider().getVisibility(), is(equalTo(View.GONE))); assertThat(viewHolder.getAction2().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getAction2Divider().getVisibility(), is(equalTo(View.GONE))); viewHolder = getViewHolderAtPosition(3); assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE))); assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.GONE))); } @Test public void testTextStartMarginMatchesPrimaryActionType() { TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, true); TextListItem item1 = new TextListItem(mActivity); item1.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); TextListItem item2 = new TextListItem(mActivity); item2.setPrimaryActionEmptyIcon(); TextListItem item3 = new TextListItem(mActivity); item3.setPrimaryActionNoIcon(); List items = Arrays.asList(item0, item1, item2, item3); List expectedStartMargin = Arrays.asList(R.dimen.car_keyline_4, R.dimen.car_keyline_3, R.dimen.car_keyline_3, R.dimen.car_keyline_1); setupPagedListView(items); for (int i = 0; i < items.size(); i++) { TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(i); int expected = InstrumentationRegistry.getContext().getResources() .getDimensionPixelSize(expectedStartMargin.get(i)); assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getTitle().getLayoutParams()) .getMarginStart(), is(equalTo(expected))); assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getBody().getLayoutParams()) .getMarginStart(), is(equalTo(expected))); } } @Test public void testItemWithOnlyTitleIsSingleLine() { // Only space. TextListItem item0 = new TextListItem(mActivity); item0.setTitle(" "); // Underscore. TextListItem item1 = new TextListItem(mActivity); item1.setTitle("______"); TextListItem item2 = new TextListItem(mActivity); item2.setTitle("ALL UPPER CASE"); // String wouldn't fit in one line. TextListItem item3 = new TextListItem(mActivity); item3.setTitle(InstrumentationRegistry.getContext().getResources().getString( R.string.over_120_chars)); List items = Arrays.asList(item0, item1, item2, item3); setupPagedListView(items); double singleLineHeight = InstrumentationRegistry.getContext().getResources().getDimension( R.dimen.car_single_line_list_item_height); LinearLayoutManager layoutManager = (LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager(); for (int i = 0; i < items.size(); i++) { assertThat((double) layoutManager.findViewByPosition(i).getHeight(), is(closeTo(singleLineHeight, 1.0d))); } } @Test public void testItemWithBodyTextIsAtLeastDoubleLine() { // Only space. TextListItem item0 = new TextListItem(mActivity); item0.setBody(" "); // Underscore. TextListItem item1 = new TextListItem(mActivity); item1.setBody("____"); // String wouldn't fit in one line. TextListItem item2 = new TextListItem(mActivity); item2.setBody(InstrumentationRegistry.getContext().getResources().getString( R.string.over_120_chars)); List items = Arrays.asList(item0, item1, item2); setupPagedListView(items); final int doubleLineHeight = (int) InstrumentationRegistry.getContext().getResources().getDimension( R.dimen.car_double_line_list_item_height); LinearLayoutManager layoutManager = (LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager(); for (int i = 0; i < items.size(); i++) { assertThat(layoutManager.findViewByPosition(i).getHeight(), is(greaterThanOrEqualTo(doubleLineHeight))); } } @Test public void testBodyTextLengthLimit() { final String longText = InstrumentationRegistry.getContext().getResources().getString( R.string.over_120_chars); final int limit = InstrumentationRegistry.getContext().getResources().getInteger( R.integer.car_list_item_text_length_limit); TextListItem item0 = new TextListItem(mActivity); item0.setBody(longText); List items = Arrays.asList(item0); setupPagedListView(items); // + 1 for appended ellipsis. assertThat(getViewHolderAtPosition(0).getBody().getText().length(), is(equalTo(limit + 1))); } @Test public void testPrimaryIconDrawable() { Drawable drawable = InstrumentationRegistry.getContext().getResources().getDrawable( android.R.drawable.sym_def_app_icon, null); TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(drawable, true); List items = Arrays.asList(item0); setupPagedListView(items); assertTrue(getViewHolderAtPosition(0).getPrimaryIcon().getDrawable().getConstantState() .equals(drawable.getConstantState())); } @Test public void testLargePrimaryIconHasNoStartMargin() { TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, true); List items = Arrays.asList(item0); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getPrimaryIcon().getLayoutParams()) .getMarginStart(), is(equalTo(0))); } @Test public void testSmallPrimaryIconStartMargin() { TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); List items = Arrays.asList(item0); setupPagedListView(items); int expected = InstrumentationRegistry.getContext().getResources().getDimensionPixelSize( R.dimen.car_keyline_1); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getPrimaryIcon().getLayoutParams()) .getMarginStart(), is(equalTo(expected))); } @Test public void testSmallPrimaryIconTopMarginRemainsTheSameRegardlessOfTextLength() { final String longText = InstrumentationRegistry.getContext().getResources().getString( R.string.over_120_chars); // Single line item. TextListItem item0 = new TextListItem(mActivity); item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); item0.setTitle("one line text"); // Double line item with one line text. TextListItem item1 = new TextListItem(mActivity); item1.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); item1.setTitle("one line text"); item1.setBody("one line text"); // Double line item with long text. TextListItem item2 = new TextListItem(mActivity); item2.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); item2.setTitle("one line text"); item2.setBody(longText); // Body text only - long text. TextListItem item3 = new TextListItem(mActivity); item3.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); item3.setBody(longText); // Body text only - one line text. TextListItem item4 = new TextListItem(mActivity); item4.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); item4.setBody("one line text"); List items = Arrays.asList(item0, item1, item2, item3, item4); setupPagedListView(items); for (int i = 1; i < items.size(); i++) { onView(withId(R.id.recycler_view)).perform(scrollToPosition(i)); // Implementation uses integer division so it may be off by 1 vs centered vertically. assertThat((double) getViewHolderAtPosition(i - 1).getPrimaryIcon().getTop(), is(closeTo( (double) getViewHolderAtPosition(i).getPrimaryIcon().getTop(), 1.0d))); } } @Test public void testClickingPrimaryActionIsSeparateFromSupplementalAction() { final boolean[] clicked = {false, false}; TextListItem item0 = new TextListItem(mActivity); item0.setOnClickListener(v -> clicked[0] = true); item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, true, v -> clicked[1] = true); List items = Arrays.asList(item0); setupPagedListView(items); onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(0, click())); assertTrue(clicked[0]); assertFalse(clicked[1]); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.supplemental_icon))); assertTrue(clicked[1]); } @Test public void testClickingSupplementalIcon() { final boolean[] clicked = {false}; TextListItem item0 = new TextListItem(mActivity); item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, true, v -> clicked[0] = true); List items = Arrays.asList(item0); setupPagedListView(items); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.supplemental_icon))); assertTrue(clicked[0]); } @Test public void testSupplementalIconWithoutClickListenerIsNotClickable() { TextListItem item0 = new TextListItem(mActivity); item0.setSupplementalIcon(android.R.drawable.sym_def_app_icon, true); List items = Arrays.asList(item0); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertFalse(viewHolder.getSupplementalIcon().isClickable()); } @Test public void testCheckingSwitch() { final boolean[] clicked = {false, false}; TextListItem item0 = new TextListItem(mActivity); item0.setSwitch(false, false, (button, isChecked) -> { // Initial value is false. assertTrue(isChecked); clicked[0] = true; }); TextListItem item1 = new TextListItem(mActivity); item1.setSwitch(true, false, (button, isChecked) -> { // Initial value is true. assertFalse(isChecked); clicked[1] = true; }); List items = Arrays.asList(item0, item1); setupPagedListView(items); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.switch_widget))); assertTrue(clicked[0]); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(1, clickChildViewWithId(R.id.switch_widget))); assertTrue(clicked[1]); } @Test public void testClickingSupplementalAction() { final boolean[] clicked = {false}; TextListItem item0 = new TextListItem(mActivity); item0.setAction("action", true, v -> clicked[0] = true); List items = Arrays.asList(item0); setupPagedListView(items); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.action1))); assertTrue(clicked[0]); } @Test public void testClickingBothSupplementalActions() { final boolean[] clicked = {false, false}; TextListItem item0 = new TextListItem(mActivity); item0.setActions("action 1", true, v -> clicked[0] = true, "action 2", true, v -> clicked[1] = true); List items = Arrays.asList(item0); setupPagedListView(items); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.action1))); assertTrue(clicked[0]); assertFalse(clicked[1]); onView(withId(R.id.recycler_view)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.action2))); assertTrue(clicked[1]); } @Test public void testCustomViewBinderBindsLast() { final String updatedTitle = "updated title"; TextListItem item0 = new TextListItem(mActivity); item0.setTitle("original title"); item0.addViewBinder((viewHolder) -> viewHolder.getTitle().setText(updatedTitle)); List items = Arrays.asList(item0); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getTitle().getText(), is(equalTo(updatedTitle))); } @Test public void testCustomViewBinderOnUnusedViewsHasNoEffect() { TextListItem item0 = new TextListItem(mActivity); item0.addViewBinder((viewHolder) -> viewHolder.getBody().setText("text")); List items = Arrays.asList(item0); setupPagedListView(items); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getBody().getVisibility(), is(equalTo(View.GONE))); // Custom binder interacts with body but has no effect. // Expect card height to remain single line. assertThat((double) viewHolder.itemView.getHeight(), is(closeTo( InstrumentationRegistry.getContext().getResources().getDimension( R.dimen.car_single_line_list_item_height), 1.0d))); } @Test public void testSettingTitleOrBodyAsPrimaryText() { // Create 2 items, one with Title as primary (default) and one with Body. // The primary text, regardless of view, should have consistent look (as primary). TextListItem item0 = new TextListItem(mActivity); item0.setTitle("title"); item0.setBody("body"); TextListItem item1 = new TextListItem(mActivity); item1.setTitle("title"); item1.setBody("body", true); List items = Arrays.asList(item0, item1); setupPagedListView(items); TextListItem.ViewHolder titlePrimary = getViewHolderAtPosition(0); TextListItem.ViewHolder bodyPrimary = getViewHolderAtPosition(1); assertThat(titlePrimary.getTitle().getTextSize(), is(equalTo(bodyPrimary.getBody().getTextSize()))); assertThat(titlePrimary.getTitle().getTextColors(), is(equalTo(bodyPrimary.getBody().getTextColors()))); } @Test public void testNoCarriedOverLayoutParamsForTextView() throws Throwable { TextListItem singleLine = new TextListItem(mActivity); singleLine.setTitle("title"); setupPagedListView(Arrays.asList(singleLine)); // Manually rebind the view holder of a single line item to a double line item. TextListItem doubleLine = new TextListItem(mActivity); doubleLine.setTitle("title"); doubleLine.setBody("body"); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); mActivityRule.runOnUiThread(() -> doubleLine.bind(viewHolder)); RelativeLayout.LayoutParams titleLayoutParams = (RelativeLayout.LayoutParams) viewHolder.getTitle().getLayoutParams(); RelativeLayout.LayoutParams bodyLayoutParams = (RelativeLayout.LayoutParams) viewHolder.getTitle().getLayoutParams(); assertThat(titleLayoutParams.getRule(RelativeLayout.CENTER_VERTICAL), is(equalTo(0))); assertThat(bodyLayoutParams.getRule(RelativeLayout.CENTER_VERTICAL), is(equalTo(0))); } @Test public void testNoCarriedOverLayoutParamsForPrimaryIcon() throws Throwable { TextListItem smallIcon = new TextListItem(mActivity); smallIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false); smallIcon.setBody("body"); // Small icon of items with body text should use top margin. setupPagedListView(Arrays.asList(smallIcon)); // Manually rebind the view holder. TextListItem largeIcon = new TextListItem(mActivity); largeIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, true); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); mActivityRule.runOnUiThread(() -> largeIcon.bind(viewHolder)); RelativeLayout.LayoutParams iconLayoutParams = (RelativeLayout.LayoutParams) viewHolder.getPrimaryIcon().getLayoutParams(); assertThat(iconLayoutParams.getRule(RelativeLayout.CENTER_VERTICAL), is(equalTo(RelativeLayout.TRUE))); assertThat(iconLayoutParams.topMargin, is(equalTo(0))); } @Test public void testUpdateItem() throws Throwable { TextListItem item = new TextListItem(mActivity); setupPagedListView(Arrays.asList(item)); String title = "updated title"; item.setTitle(title); mActivityRule.runOnUiThread(() -> mPagedListView.getAdapter().notifyItemChanged(0)); // Wait for paged list view to layout by using espresso to scroll to a position. onView(withId(R.id.recycler_view)).perform(scrollToPosition(0)); TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0); assertThat(viewHolder.getTitle().getText(), is(equalTo(title))); } private static ViewAction clickChildViewWithId(final int id) { return new ViewAction() { @Override public Matcher getConstraints() { return null; } @Override public String getDescription() { return "Click on a child view with specific id."; } @Override public void perform(UiController uiController, View view) { View v = view.findViewById(id); v.performClick(); } }; } }