| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.autofill_assistant; |
| |
| import static androidx.test.espresso.Espresso.onView; |
| import static androidx.test.espresso.action.ViewActions.swipeRight; |
| import static androidx.test.espresso.assertion.PositionAssertions.isRightOf; |
| import static androidx.test.espresso.assertion.ViewAssertions.matches; |
| import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; |
| import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; |
| import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; |
| import static androidx.test.espresso.matcher.ViewMatchers.withText; |
| |
| import static org.hamcrest.CoreMatchers.is; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.allOf; |
| |
| import android.support.test.InstrumentationRegistry; |
| import android.widget.LinearLayout; |
| |
| import androidx.test.filters.MediumTest; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.test.util.CommandLineFlags; |
| import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantActionsCarouselCoordinator; |
| import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantCarouselModel; |
| import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip; |
| import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule; |
| import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils; |
| import org.chromium.chrome.browser.flags.ChromeSwitches; |
| import org.chromium.chrome.test.ChromeJUnit4ClassRunner; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Tests for the autofill assistant actions carousel. |
| */ |
| @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) |
| @RunWith(ChromeJUnit4ClassRunner.class) |
| public class AutofillAssistantActionsCarouselUiTest { |
| @Rule |
| public CustomTabActivityTestRule mTestRule = new CustomTabActivityTestRule(); |
| |
| /** Creates a coordinator for use in UI tests, and adds it to the global view hierarchy. */ |
| private AssistantActionsCarouselCoordinator createCoordinator(AssistantCarouselModel model) |
| throws Exception { |
| AssistantActionsCarouselCoordinator coordinator = TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> new AssistantActionsCarouselCoordinator( |
| InstrumentationRegistry.getTargetContext(), model)); |
| |
| TestThreadUtils.runOnUiThreadBlocking(() -> { |
| // Note: apparently, we need an intermediate container for this coordinator's view, |
| // otherwise the view will be invisible. |
| // @TODO(crbug.com/806868) figure out why this is the case. |
| LinearLayout container = new LinearLayout(InstrumentationRegistry.getTargetContext()); |
| container.addView(coordinator.getView()); |
| AutofillAssistantUiTestUtil.attachToCoordinator(mTestRule.getActivity(), container); |
| }); |
| |
| return coordinator; |
| } |
| |
| @Before |
| public void setUp() { |
| mTestRule.startCustomTabActivityWithIntent(CustomTabsTestUtils.createMinimalCustomTabIntent( |
| InstrumentationRegistry.getTargetContext(), "about:blank")); |
| } |
| |
| /** Tests assumptions about the initial state of the carousel. */ |
| @Test |
| @MediumTest |
| public void testInitialState() throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| assertThat(model.get(AssistantCarouselModel.CHIPS).size(), is(0)); |
| assertThat(coordinator.getView().getAdapter().getItemCount(), is(0)); |
| } |
| |
| /** Adds a single chip and tests assumptions about the view state after the change. */ |
| @Test |
| @MediumTest |
| public void testAddSingleChip() throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> model.set(AssistantCarouselModel.CHIPS, |
| Collections.singletonList(new AssistantChip( |
| AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Test", false, true, true, "")))); |
| |
| // Chip was created and is displayed on the screen. |
| onView(is(coordinator.getView())) |
| .check(matches(hasDescendant(allOf(withText("Test"), isDisplayed())))); |
| |
| // TODO(crbug.com/806868): test that single chip is center aligned. |
| } |
| |
| /** Adds multiple chips and tests assumptions about the view state after the change. */ |
| @Test |
| @MediumTest |
| public void testAddMultipleChips() throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| // Note: this should be a small number that fits on screen without scrolling. |
| int numChips = 3; |
| List<AssistantChip> chips = new ArrayList<>(); |
| for (int i = 0; i < numChips; i++) { |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "T" + i, false, false, true, "")); |
| } |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "X", false, true, true, "")); |
| |
| TestThreadUtils.runOnUiThreadBlocking(() -> model.set(AssistantCarouselModel.CHIPS, chips)); |
| |
| // Cancel chip is displayed to the user. |
| onView(withText("X")).check(matches(isDisplayed())); |
| |
| // All chips are to the right of the cancel chip. |
| for (int i = 0; i < numChips; i++) { |
| onView(withText("T" + i)).check(isRightOf(withText("X"))); |
| } |
| } |
| |
| /** Adds many chips and tests that the cancel chip is always visible. */ |
| @Test |
| @MediumTest |
| public void testCancelChipAlwaysVisible() throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| // Note: this should be a large number that does not fit on screen without scrolling. |
| int numChips = 30; |
| List<AssistantChip> chips = new ArrayList<>(); |
| for (int i = 0; i < numChips; i++) { |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Test" + i, false, false, true, "")); |
| } |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Cancel", false, true, true, "")); |
| TestThreadUtils.runOnUiThreadBlocking(() -> model.set(AssistantCarouselModel.CHIPS, chips)); |
| |
| // Cancel chip is initially displayed to the user. |
| onView(withText("Cancel")).check(matches(isDisplayed())); |
| |
| // Scroll right, check that cancel is still visible. |
| onView(is(coordinator.getView())).perform(swipeRight()); |
| onView(withText("Cancel")).check(matches(isDisplayed())); |
| } |
| |
| /** |
| * Tests the change between two chip configurations: |
| * X Test_2 |
| * X Test_1 Test_2 |
| * |
| * This inserts Test_1 in between X and Test_2, forcing Test_2 to move. |
| */ |
| @Test |
| @MediumTest |
| public void testMoveChip() throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| List<AssistantChip> chips = new ArrayList<>(); |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Test 2", false, false, true, "")); |
| chips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Cancel", false, true, true, "")); |
| TestThreadUtils.runOnUiThreadBlocking(() -> model.set(AssistantCarouselModel.CHIPS, chips)); |
| onView(withText("Cancel")).check(matches(isDisplayed())); |
| onView(withText("Test 2")).check(matches(isDisplayed())); |
| onView(withText("Test 2")).check(isRightOf(withText("Cancel"))); |
| |
| List<AssistantChip> newChips = new ArrayList<>(); |
| newChips.add(chips.get(0)); |
| newChips.add(new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Test 1", false, false, true, "")); |
| newChips.add(chips.get(1)); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> model.set(AssistantCarouselModel.CHIPS, newChips)); |
| onView(withText("Cancel")).check(matches(isDisplayed())); |
| onView(withText("Test 1")).check(matches(isDisplayed())); |
| onView(withText("Test 2")).check(matches(isDisplayed())); |
| onView(withText("Test 1")).check(isRightOf(withText("Cancel"))); |
| onView(withText("Test 2")).check(isRightOf(withText("Test 1"))); |
| } |
| |
| /** |
| * Adds a single chip with non empty content description, and tests that same is used as content |
| * description. |
| */ |
| @Test |
| @MediumTest |
| public void testSuppliedNonEmptyContentDescriptionIsUsed() throws Exception { |
| String contentDescription = "Test content description"; |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> model.set(AssistantCarouselModel.CHIPS, |
| Collections.singletonList(new AssistantChip( |
| AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Chip", false, true, true, contentDescription)))); |
| |
| onView(is(coordinator.getView())) |
| .check(matches(hasDescendant( |
| allOf(withContentDescription(contentDescription), isDisplayed())))); |
| } |
| |
| /** |
| * Adds a single chip with empty content description, and tests that same is used as content |
| * description. |
| */ |
| @Test |
| @MediumTest |
| public void testSuppliedEmptyContentDescriptionIsUsed() throws Exception { |
| String contentDescription = ""; |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinator = createCoordinator(model); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> model.set(AssistantCarouselModel.CHIPS, |
| Collections.singletonList(new AssistantChip( |
| AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.NONE, |
| "Chip", false, true, true, contentDescription)))); |
| |
| onView(is(coordinator.getView())) |
| .check(matches(hasDescendant( |
| allOf(withContentDescription(contentDescription), isDisplayed())))); |
| } |
| |
| /** |
| * Adds a single chip with null content description, and tests that chip text is used as content |
| * description if it's non-empty. |
| */ |
| @Test |
| @MediumTest |
| public void testWhenNullContentDescriptionIsSuppliedChipTextIsUsed() throws Exception { |
| String chipText = "Chip Text"; |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinatorNonEmptyChipText = createCoordinator(model); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> model.set(AssistantCarouselModel.CHIPS, |
| Collections.singletonList( |
| new AssistantChip(AssistantChip.Type.BUTTON_HAIRLINE, |
| AssistantChip.Icon.DONE, chipText, false, true, |
| true, /* contentDescription */ null)))); |
| |
| onView(is(coordinatorNonEmptyChipText.getView())) |
| .check(matches( |
| hasDescendant(allOf(withContentDescription(chipText), isDisplayed())))); |
| } |
| |
| /** |
| * Adds a single chip with null content description and empty chip text, and tests that icon |
| * description is used as content description if available. |
| */ |
| @Test |
| @MediumTest |
| public void testWhenNullContentDescriptionIsSuppliedChipTextOrIconDescriptionIsUsed() |
| throws Exception { |
| AssistantCarouselModel model = |
| TestThreadUtils.runOnUiThreadBlocking(AssistantCarouselModel::new); |
| AssistantActionsCarouselCoordinator coordinatorEmptyChipText = createCoordinator(model); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> model.set(AssistantCarouselModel.CHIPS, |
| Collections.singletonList(new AssistantChip( |
| AssistantChip.Type.BUTTON_HAIRLINE, AssistantChip.Icon.DONE, |
| /* chipText */ "", false, true, true, |
| /* contentDescription */ null)))); |
| |
| onView(is(coordinatorEmptyChipText.getView())) |
| .check(matches( |
| hasDescendant(allOf(withContentDescription("Done"), isDisplayed())))); |
| } |
| } |