chromium/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkModelTest.java

// Copyright 2015 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.bookmarks;

import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.profiles.Profile;
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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/** Tests for {@link BookmarkModel}, the data layer of bookmarks. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class BookmarkModelTest {
    public static final GURL A_COM = new GURL("http://a.com");
    public static final GURL B_COM = new GURL("http://b.com");
    public static final GURL C_COM = new GURL("http://c.com");
    public static final GURL AA_COM = new GURL("http://aa.com");

    @Rule public final ChromeBrowserTestRule mChromeBrowserTestRule = new ChromeBrowserTestRule();

    private static final int TIMEOUT_MS = 5000;
    private BookmarkModel mBookmarkModel;
    private BookmarkId mMobileNode;
    private BookmarkId mOtherNode;
    private BookmarkId mDesktopNode;

    @Before
    public void setUp() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Profile profile = ProfileManager.getLastUsedRegularProfile();
                    mBookmarkModel = BookmarkModel.getForProfile(profile);
                    mBookmarkModel.loadEmptyPartnerBookmarkShimForTesting();
                });

        BookmarkTestUtil.waitForBookmarkModelLoaded();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mMobileNode = mBookmarkModel.getMobileFolderId();
                    mDesktopNode = mBookmarkModel.getDesktopFolderId();
                    mOtherNode = mBookmarkModel.getOtherFolderId();
                });
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> mBookmarkModel.removeAllUserBookmarks());
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testBookmarkPropertySetters() {
        BookmarkId folderA = mBookmarkModel.addFolder(mMobileNode, 0, "a");

        BookmarkId bookmarkA = addBookmark(mDesktopNode, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(mMobileNode, 0, "a", A_COM);
        BookmarkId bookmarkC = addBookmark(mOtherNode, 0, "a", A_COM);
        BookmarkId bookmarkD = addBookmark(folderA, 0, "a", A_COM);

        mBookmarkModel.setBookmarkTitle(folderA, "hauri");
        Assert.assertEquals("hauri", mBookmarkModel.getBookmarkTitle(folderA));

        mBookmarkModel.setBookmarkTitle(bookmarkA, "auri");
        mBookmarkModel.setBookmarkUrl(bookmarkA, new GURL("http://auri.org/"));
        verifyBookmark(bookmarkA, "auri", "http://auri.org/", false, mDesktopNode);

        mBookmarkModel.setBookmarkTitle(bookmarkB, "lauri");
        mBookmarkModel.setBookmarkUrl(bookmarkB, new GURL("http://lauri.org/"));
        verifyBookmark(bookmarkB, "lauri", "http://lauri.org/", false, mMobileNode);

        mBookmarkModel.setBookmarkTitle(bookmarkC, "mauri");
        mBookmarkModel.setBookmarkUrl(bookmarkC, new GURL("http://mauri.org/"));
        verifyBookmark(bookmarkC, "mauri", "http://mauri.org/", false, mOtherNode);

        mBookmarkModel.setBookmarkTitle(bookmarkD, "kauri");
        mBookmarkModel.setBookmarkUrl(bookmarkD, new GURL("http://kauri.org/"));
        verifyBookmark(bookmarkD, "kauri", "http://kauri.org/", false, folderA);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testMoveBookmarks() {
        BookmarkId bookmarkA = addBookmark(mDesktopNode, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(mOtherNode, 0, "b", B_COM);
        BookmarkId bookmarkC = addBookmark(mMobileNode, 0, "c", C_COM);
        BookmarkId folderA = mBookmarkModel.addFolder(mOtherNode, 0, "fa");
        BookmarkId folderB = mBookmarkModel.addFolder(mDesktopNode, 0, "fb");
        BookmarkId folderC = mBookmarkModel.addFolder(mMobileNode, 0, "fc");
        BookmarkId bookmarkAA = addBookmark(folderA, 0, "aa", AA_COM);
        BookmarkId folderAA = mBookmarkModel.addFolder(folderA, 0, "faa");

        HashSet<BookmarkId> movedBookmarks = new HashSet<>(6);
        movedBookmarks.add(bookmarkA);
        movedBookmarks.add(bookmarkB);
        movedBookmarks.add(bookmarkC);
        movedBookmarks.add(folderC);
        movedBookmarks.add(folderB);
        movedBookmarks.add(bookmarkAA);
        mBookmarkModel.moveBookmarks(new ArrayList<>(movedBookmarks), folderAA);

        // Order of the moved bookmarks is not tested.
        verifyBookmarkListNoOrder(mBookmarkModel.getChildIds(folderAA), movedBookmarks);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testMoveBookmarksToSameFolder() {
        BookmarkId folder = mBookmarkModel.addFolder(mMobileNode, 0, "fc");
        BookmarkId bookmarkA = addBookmark(folder, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(folder, 1, "b", B_COM);

        HashSet<BookmarkId> movedBookmarks = new HashSet<>(2);
        movedBookmarks.add(bookmarkA);
        movedBookmarks.add(bookmarkB);
        mBookmarkModel.moveBookmarks(new ArrayList<>(movedBookmarks), folder);

        // Order of the moved bookmarks is not tested.
        verifyBookmarkListNoOrder(mBookmarkModel.getChildIds(folder), movedBookmarks);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testMoveBookmarksMixed() {
        // Inspired by https://crbug.com/1441847 where a move during a search would have bookmarks
        // from a mixed set of parent folders. Need to be able to handle interleaving url bookmarks
        // where only some of which are in the same destination folder.
        BookmarkId folderA = mBookmarkModel.addFolder(mMobileNode, 0, "fa");
        BookmarkId folderC = mBookmarkModel.addFolder(mMobileNode, 0, "fc");
        BookmarkId bookmarkA = addBookmark(folderA, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(folderA, 1, "b", B_COM);
        BookmarkId bookmarkC = addBookmark(folderC, 0, "c", C_COM);

        List<BookmarkId> movedBookmarks = new ArrayList<>();
        movedBookmarks.add(bookmarkA);
        movedBookmarks.add(bookmarkC);
        movedBookmarks.add(bookmarkB);
        mBookmarkModel.moveBookmarks(movedBookmarks, folderC);

        verifyBookmarkListNoOrder(mBookmarkModel.getChildIds(folderA), Collections.emptyList());
        verifyBookmarkListNoOrder(mBookmarkModel.getChildIds(folderC), movedBookmarks);
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testDeleteBookmarks() {
        BookmarkId bookmarkA = addBookmark(mDesktopNode, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(mOtherNode, 0, "b", B_COM);
        BookmarkId bookmarkC = addBookmark(mMobileNode, 0, "c", C_COM);

        // Delete a single bookmark.
        mBookmarkModel.deleteBookmarks(bookmarkA);
        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkA));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkB));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkC));

        mBookmarkModel.undo();
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkA));

        // Delete and undo deletion of multiple bookmarks.
        mBookmarkModel.deleteBookmarks(bookmarkA, bookmarkB);

        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkA));
        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkB));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkC));

        mBookmarkModel.undo();

        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkA));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkB));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkC));
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testDeleteBookmarksRepeatedly() {
        BookmarkId bookmarkA = addBookmark(mDesktopNode, 0, "a", A_COM);
        BookmarkId bookmarkB = addBookmark(mOtherNode, 0, "b", B_COM);
        BookmarkId bookmarkC = addBookmark(mMobileNode, 0, "c", C_COM);

        mBookmarkModel.deleteBookmarks(bookmarkA);

        // This line is problematic, see: https://crbug.com/824559
        mBookmarkModel.deleteBookmarks(bookmarkA, bookmarkB);

        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkA));
        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkB));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkC));

        // Only bookmark B should be undeleted here.
        mBookmarkModel.undo();

        Assert.assertNull(mBookmarkModel.getBookmarkById(bookmarkA));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkB));
        Assert.assertNotNull(mBookmarkModel.getBookmarkById(bookmarkC));
    }

    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testGetChildIDs() {
        BookmarkId folderA = mBookmarkModel.addFolder(mMobileNode, 0, "fa");
        HashSet<BookmarkId> expectedChildren = new HashSet<>();
        expectedChildren.add(addBookmark(folderA, 0, "a", A_COM));
        expectedChildren.add(addBookmark(folderA, 0, "a", A_COM));
        expectedChildren.add(addBookmark(folderA, 0, "a", A_COM));
        expectedChildren.add(addBookmark(folderA, 0, "a", A_COM));
        BookmarkId folderAA = mBookmarkModel.addFolder(folderA, 0, "faa");
        // folders and urls
        expectedChildren.add(folderAA);
        verifyBookmarkListNoOrder(mBookmarkModel.getChildIds(folderA), expectedChildren);
    }

    // Moved from BookmarkBridgeTest
    @Test
    @SmallTest
    @UiThreadTest
    @Feature({"Bookmark"})
    public void testAddBookmarksAndFolders() {
        BookmarkId bookmarkA = addBookmark(mDesktopNode, 0, "a", A_COM);
        verifyBookmark(bookmarkA, "a", "http://a.com/", false, mDesktopNode);

        BookmarkId bookmarkB = addBookmark(mOtherNode, 0, "b", B_COM);
        verifyBookmark(bookmarkB, "b", "http://b.com/", false, mOtherNode);

        BookmarkId bookmarkC = addBookmark(mMobileNode, 0, "c", C_COM);
        verifyBookmark(bookmarkC, "c", "http://c.com/", false, mMobileNode);

        BookmarkId folderA = mBookmarkModel.addFolder(mOtherNode, 0, "fa");
        verifyBookmark(folderA, "fa", null, true, mOtherNode);

        BookmarkId folderB = mBookmarkModel.addFolder(mDesktopNode, 0, "fb");
        verifyBookmark(folderB, "fb", null, true, mDesktopNode);

        BookmarkId folderC = mBookmarkModel.addFolder(mMobileNode, 0, "fc");
        verifyBookmark(folderC, "fc", null, true, mMobileNode);

        BookmarkId bookmarkAA = addBookmark(folderA, 0, "aa", AA_COM);
        verifyBookmark(bookmarkAA, "aa", "http://aa.com/", false, folderA);

        BookmarkId folderAA = mBookmarkModel.addFolder(folderA, 0, "faa");
        verifyBookmark(folderAA, "faa", null, true, folderA);
    }

    private BookmarkId addBookmark(
            final BookmarkId parent, final int index, final String title, final GURL url) {
        return addBookmark(mBookmarkModel, parent, index, title, url);
    }

    public static BookmarkId addBookmark(
            BookmarkModel model,
            final BookmarkId parent,
            final int index,
            final String title,
            final GURL url) {
        final AtomicReference<BookmarkId> result = new AtomicReference<>();
        final Semaphore semaphore = new Semaphore(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    result.set(model.addBookmark(parent, index, title, url));
                    semaphore.release();
                });
        try {
            if (semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                return result.get();
            } else {
                return null;
            }
        } catch (InterruptedException e) {
            return null;
        }
    }

    private void verifyBookmark(
            BookmarkId idToVerify,
            String expectedTitle,
            String expectedUrl,
            boolean isFolder,
            BookmarkId expectedParent) {
        Assert.assertNotNull(idToVerify);
        BookmarkItem item = mBookmarkModel.getBookmarkById(idToVerify);
        Assert.assertEquals(expectedTitle, item.getTitle());
        Assert.assertEquals(isFolder, item.isFolder());
        if (!isFolder) Assert.assertEquals(expectedUrl, item.getUrl().getSpec());
        Assert.assertEquals(expectedParent, item.getParentId());
    }

    /**
     * Before using this helper method, always make sure @param listToVerify does not contain
     * duplicates.
     */
    private void verifyBookmarkListNoOrder(
            List<BookmarkId> listToVerify, Collection<BookmarkId> expectedIds) {
        HashSet<BookmarkId> expectedIdsCopy = new HashSet<>(expectedIds);
        Assert.assertEquals(expectedIdsCopy.size(), listToVerify.size());
        for (BookmarkId id : listToVerify) {
            Assert.assertNotNull(id);
            Assert.assertTrue("List contains wrong element: ", expectedIdsCopy.contains(id));
            expectedIdsCopy.remove(id);
        }
        Assert.assertTrue(
                "List does not contain some expected bookmarks: ", expectedIdsCopy.isEmpty());
    }
}