chromium/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkEditTest.java

// Copyright 2020 The Chromium Authors
// 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.app.bookmarks;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.MenuItem;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.bookmarks.BookmarkModelObserver;
import org.chromium.chrome.browser.bookmarks.BookmarkModelTest;
import org.chromium.chrome.browser.bookmarks.ImprovedBookmarkRowProperties;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeBrowserTestRule;
import org.chromium.chrome.test.util.BookmarkTestUtil;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.url.GURL;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/** Tests functionality in BookmarkEditActivity. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@EnableFeatures(ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS)
public class BookmarkEditTest {
    @Rule public final ChromeBrowserTestRule mChromeBrowserTestRule = new ChromeBrowserTestRule();

    private static final String TITLE_A = "a";
    private static final String TITLE_B = "b";
    private static final String URL_A = "http://a.com/";
    private static final String URL_B = "http://b.com/";
    private static final String FOLDER_A = "FolderA";
    private static BookmarkModel sBookmarkModel;
    private static BookmarkModelObserver sModelObserver;
    private static BookmarkId sBookmarkId;
    private static BookmarkId sMobileNode;
    private static BookmarkId sOtherNode;
    private static BookmarkEditActivity sBookmarkEditActivity;

    private static CallbackHelper sDestroyedCallback = new CallbackHelper();
    private static ActivityStateListener sActivityStateListener =
            new ActivityStateListener() {
                @Override
                public void onActivityStateChange(Activity activity, int newState) {
                    if (newState == ActivityState.DESTROYED) sDestroyedCallback.notifyCalled();
                }
            };
    private CallbackHelper mModelChangedCallback = new CallbackHelper();

    @Before
    public void setUp() throws TimeoutException {
        if (sBookmarkEditActivity == null) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        sBookmarkModel =
                                BookmarkModel.getForProfile(
                                        ProfileManager.getLastUsedRegularProfile());
                        sBookmarkModel.loadEmptyPartnerBookmarkShimForTesting();
                    });

            BookmarkTestUtil.waitForBookmarkModelLoaded();
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        sMobileNode = sBookmarkModel.getMobileFolderId();
                        sOtherNode = sBookmarkModel.getOtherFolderId();
                    });
            sBookmarkId =
                    BookmarkModelTest.addBookmark(
                            sBookmarkModel, sMobileNode, 0, TITLE_A, new GURL(URL_A));

            sModelObserver =
                    new BookmarkModelObserver() {
                        @Override
                        public void bookmarkModelChanged() {
                            mModelChangedCallback.notifyCalled();
                        }
                    };
            ThreadUtils.runOnUiThreadBlocking(() -> sBookmarkModel.addObserver(sModelObserver));

            startEditActivity(sBookmarkId);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        ApplicationStatus.registerStateListenerForActivity(
                                sActivityStateListener, sBookmarkEditActivity);
                    });
        }
    }

    @After
    public void resetBookmark() throws ExecutionException {
        if (getBookmarkItem(sBookmarkId) != null) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        sBookmarkModel.setBookmarkTitle(sBookmarkId, TITLE_A);
                        sBookmarkModel.setBookmarkUrl(sBookmarkId, new GURL(URL_A));
                        sBookmarkModel.moveBookmark(sBookmarkId, sMobileNode, 0);
                    });
        }
        if (sBookmarkEditActivity != null) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        sBookmarkEditActivity.getTitleEditText().getEditText().setText(TITLE_A);
                        sBookmarkEditActivity.getUrlEditText().getEditText().setText(URL_A);
                    });
        }
    }

    @AfterClass
    public static void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkModel.removeObserver(sModelObserver);
                    sBookmarkModel.removeAllUserBookmarks();
                    ApplicationStatus.unregisterActivityStateListener(sActivityStateListener);
                });
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    public void testEditTitleAndUrl() throws ExecutionException, TimeoutException {
        Assert.assertEquals(
                "Incorrect title.",
                TITLE_A,
                sBookmarkEditActivity.getTitleEditText().getEditText().getText().toString());
        Assert.assertEquals(
                "Incorrect url.",
                URL_A,
                sBookmarkEditActivity.getUrlEditText().getEditText().getText().toString());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkEditActivity.getTitleEditText().getEditText().setText(TITLE_B);
                    sBookmarkEditActivity.getUrlEditText().getEditText().setText(URL_B);
                    sBookmarkEditActivity.onStop();
                });

        BookmarkItem bookmarkItem = getBookmarkItem(sBookmarkId);
        Assert.assertEquals("Incorrect title after edit.", TITLE_B, bookmarkItem.getTitle());
        Assert.assertEquals("Incorrect url after edit.", URL_B, bookmarkItem.getUrl().getSpec());
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    public void testEditEmptyInputRejected() throws ExecutionException, TimeoutException {
        Assert.assertEquals(
                "Incorrect title.",
                TITLE_A,
                sBookmarkEditActivity.getTitleEditText().getEditText().getText().toString());
        Assert.assertEquals(
                "Incorrect url.",
                URL_A,
                sBookmarkEditActivity.getUrlEditText().getEditText().getText().toString());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkEditActivity.getTitleEditText().getEditText().setText("");
                    sBookmarkEditActivity.getUrlEditText().getEditText().setText("");
                    sBookmarkEditActivity.onStop();
                });

        BookmarkItem bookmarkItem = getBookmarkItem(sBookmarkId);
        Assert.assertEquals("Incorrect title after edit.", TITLE_A, bookmarkItem.getTitle());
        Assert.assertEquals("Incorrect url after edit.", URL_A, bookmarkItem.getUrl().getSpec());
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    public void testInvalidUrlRejected() throws ExecutionException, TimeoutException {
        Assert.assertEquals(
                "Incorrect url.",
                URL_A,
                sBookmarkEditActivity.getUrlEditText().getEditText().getText().toString());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkEditActivity
                            .getUrlEditText()
                            .getEditText()
                            .setText("http:://?foo=bar");
                    sBookmarkEditActivity.onStop();
                });

        BookmarkItem bookmarkItem = getBookmarkItem(sBookmarkId);
        Assert.assertEquals("Incorrect url after edit.", URL_A, bookmarkItem.getUrl().getSpec());
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    @RequiresRestart("tests destruction of BookmarkEditActivity")
    public void testEditActivityDeleteButton() throws ExecutionException, TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkEditActivity.onOptionsItemSelected(
                            sBookmarkEditActivity.getDeleteButton());
                });
        sDestroyedCallback.waitForCallback(0);

        BookmarkItem bookmarkItem = getBookmarkItem(sBookmarkId);
        Assert.assertNull("Bookmark item should have been deleted.", bookmarkItem);
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    @RequiresRestart("tests destruction of BookmarkEditActivity")
    public void testEditActivityHomeButton() throws ExecutionException, TimeoutException {
        MenuItem item = Mockito.mock(MenuItem.class);
        Mockito.when(item.getItemId()).thenReturn(android.R.id.home);
        ThreadUtils.runOnUiThreadBlocking(() -> sBookmarkEditActivity.onOptionsItemSelected(item));

        Assert.assertTrue(
                "BookmarkActivity should be finishing or destroyed.",
                sBookmarkEditActivity.isFinishing() || sBookmarkEditActivity.isDestroyed());
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    public void testEditActivityReflectsModelChanges() throws TimeoutException, ExecutionException {
        Assert.assertEquals(
                "Incorrect title.",
                TITLE_A,
                sBookmarkEditActivity.getTitleEditText().getEditText().getText().toString());
        Assert.assertEquals(
                "Incorrect folder.",
                getBookmarkItem(sMobileNode).getTitle(),
                sBookmarkEditActivity
                        .getFolderSelectRowPropertyModelForTesting()
                        .get(ImprovedBookmarkRowProperties.TITLE));

        int currentModelChangedCount = mModelChangedCallback.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sBookmarkModel.setBookmarkTitle(sBookmarkId, TITLE_B);
                    sBookmarkModel.moveBookmark(sBookmarkId, sOtherNode, 0);
                });
        mModelChangedCallback.waitForCallback(currentModelChangedCount);

        Assert.assertEquals(
                "Title shouldn't change after model update.",
                TITLE_A,
                sBookmarkEditActivity.getTitleEditText().getEditText().getText().toString());
        Assert.assertEquals(
                "Folder should change after model update.",
                getBookmarkItem(sOtherNode).getTitle(),
                sBookmarkEditActivity
                        .getFolderSelectRowPropertyModelForTesting()
                        .get(ImprovedBookmarkRowProperties.TITLE));
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    @RequiresRestart("tests destruction of BookmarkEditActivity")
    public void testEditActivityFinishesWhenBookmarkDeleted() throws TimeoutException {
        int currentModelChangedCount = mModelChangedCallback.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(() -> sBookmarkModel.deleteBookmark(sBookmarkId));
        mModelChangedCallback.waitForCallback(currentModelChangedCount);

        Assert.assertTrue(
                "BookmarkActivity should be finishing or destroyed.",
                sBookmarkEditActivity.isFinishing() || sBookmarkEditActivity.isDestroyed());
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    public void testEditFolderLocation()
            throws ExecutionException, TimeoutException, InterruptedException {
        addFolder(sMobileNode, 0, FOLDER_A);
        ThreadUtils.runOnUiThreadBlocking(
                () -> sBookmarkEditActivity.getFolderSelectRowForTesting().performClick());
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ApplicationStatus.getLastTrackedFocusedActivity(),
                            IsInstanceOf.instanceOf(BookmarkFolderPickerActivity.class));
                });

        // This sleep allows the espresso events to actually go through. Without them, the test is
        // very flaky.
        Thread.sleep(200);
        onView(withText("Mobile bookmarks")).perform(click());
        onView(withText(FOLDER_A)).perform(click());
        onView(withText("Move here")).perform(click());
        waitForEditActivity();
        Assert.assertEquals(
                "Folder should change after folder activity finishes.",
                FOLDER_A,
                sBookmarkEditActivity
                        .getFolderSelectRowPropertyModelForTesting()
                        .get(ImprovedBookmarkRowProperties.TITLE));
    }

    @Test
    @MediumTest
    @Feature({"Bookmark"})
    @RequiresRestart("tests destruction of BookmarkEditActivity")
    public void testChangeFolderWhenBookmarkRemoved() throws ExecutionException, TimeoutException {
        ThreadUtils.runOnUiThreadBlocking(
                () -> sBookmarkEditActivity.getFolderSelectRowForTesting().performClick());
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ApplicationStatus.getLastTrackedFocusedActivity(),
                            IsInstanceOf.instanceOf(BookmarkFolderPickerActivity.class));
                });

        ThreadUtils.runOnUiThreadBlocking(() -> sBookmarkModel.deleteBookmark(sBookmarkId));
        CriteriaHelper.pollUiThread(
                () ->
                        !(ApplicationStatus.getLastTrackedFocusedActivity()
                                instanceof BookmarkFolderPickerActivity),
                "Timed out waiting for BookmarkFolderSelectActivity to close");
    }

    private BookmarkItem getBookmarkItem(BookmarkId bookmarkId) throws ExecutionException {
        return ThreadUtils.runOnUiThreadBlocking(() -> sBookmarkModel.getBookmarkById(bookmarkId));
    }

    private static void startEditActivity(BookmarkId bookmarkId) {
        Context context = ApplicationProvider.getApplicationContext();
        Intent intent = new Intent(context, BookmarkEditActivity.class);
        intent.putExtra(BookmarkEditActivity.INTENT_BOOKMARK_ID, bookmarkId.toString());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        sBookmarkEditActivity =
                (BookmarkEditActivity)
                        InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
    }

    private BookmarkId addFolder(BookmarkId parent, int index, String title)
            throws ExecutionException {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> sBookmarkModel.addFolder(parent, index, title));
    }

    private void waitForEditActivity() {
        CriteriaHelper.pollUiThread(
                () ->
                        ApplicationStatus.getLastTrackedFocusedActivity()
                                instanceof BookmarkEditActivity,
                "Timed out waiting for BookmarkEditActivity");
        sBookmarkEditActivity =
                (BookmarkEditActivity) ApplicationStatus.getLastTrackedFocusedActivity();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }
}