chromium/chrome/test/data/webui/side_panel/bookmarks/power_bookmarks_service_test.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://bookmarks-side-panel.top-chrome/power_bookmarks_list.js';

import type {BookmarkProductInfo} from '//resources/cr_components/commerce/shopping_service.mojom-webui.js';
import {BookmarksApiProxyImpl} from 'chrome://bookmarks-side-panel.top-chrome/bookmarks_api_proxy.js';
import {PowerBookmarksService} from 'chrome://bookmarks-side-panel.top-chrome/power_bookmarks_service.js';
import {BrowserProxyImpl as ShoppingServiceApiProxyImpl} from 'chrome://resources/cr_components/commerce/browser_proxy.js';
import {PageImageServiceBrowserProxy} from 'chrome://resources/cr_components/page_image_service/browser_proxy.js';
import {PageImageServiceHandlerRemote} from 'chrome://resources/cr_components/page_image_service/page_image_service.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
import {assertEquals} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
import {TestPluralStringProxy} from 'chrome://webui-test/test_plural_string_proxy.js';

import {TestBrowserProxy as TestShoppingServiceApiProxy} from './commerce/test_shopping_service_api_proxy.js';
import {TestBookmarksApiProxy} from './test_bookmarks_api_proxy.js';
import {TestPowerBookmarksDelegate} from './test_power_bookmarks_delegate.js';

class ServiceTestPowerBookmarksDelegate extends TestPowerBookmarksDelegate {
  override getTrackedProductInfos() {
    const productInfo = {
      title: 'Sample Product',
      clusterTitle: 'Sample Cluster',
      domain: 'sampledomain.com',
      imageUrl: {url: 'http://example.com/sample.jpg'},
      productUrl: {url: 'http://example.com/sample-product'},
      currentPrice: '29.99',
      previousPrice: '39.99',
      clusterId: BigInt(1),
      categoryLabels: ['electronics', 'gadgets'],
      price: '29.99',
      rating: '4.5',
      description: 'This is a sample product description.',
    };

    const bookmarkProductInfo: BookmarkProductInfo = {
      bookmarkId: BigInt(3),
      info: productInfo,
    };
    this.methodCalled('getTrackedProductInfos');

    const trackedProductInfos = {'3': bookmarkProductInfo};
    return trackedProductInfos;
  }
}

suite('SidePanelPowerBookmarksServiceTest', () => {
  let delegate: ServiceTestPowerBookmarksDelegate;
  let service: PowerBookmarksService;
  let bookmarksApi: TestBookmarksApiProxy;
  let shoppingServiceApi: TestShoppingServiceApiProxy;
  let imageServiceHandler: TestMock<PageImageServiceHandlerRemote>&
      PageImageServiceHandlerRemote;

  const folders: chrome.bookmarks.BookmarkTreeNode[] = [
    {
      id: '2',
      parentId: '0',
      title: 'Other Bookmarks',
      children: [
        {
          id: '3',
          parentId: '2',
          title: 'First child bookmark',
          url: 'http://child/bookmark/1/',
          dateAdded: 1,
          dateLastUsed: 4,
        },
        {
          id: '4',
          parentId: '2',
          title: 'Second child bookmark',
          url: 'http://child/bookmark/2/',
          dateAdded: 3,
          dateLastUsed: 3,
        },
        {
          id: '5',
          parentId: '2',
          title: 'Child folder',
          dateAdded: 2,
          children: [
            {
              id: '6',
              parentId: '5',
              title: 'Nested bookmark',
              url: 'http://nested/bookmark/',
              dateAdded: 4,
            },
          ],
        },
      ],
    },
  ];

  // More complex folder structure, including a more extensive hierarchy and
  // inverted dateAdded between parents/children. Not used by default in tests.
  const complexFolders: chrome.bookmarks.BookmarkTreeNode[] = [
    {
      id: '2',
      parentId: '0',
      title: 'Other Bookmarks',
      children: [
        {
          id: '3',
          parentId: '2',
          title: 'Child folder',
          dateAdded: 4,
          children: [
            {
              id: '7',
              parentId: '3',
              title: 'Nested bookmark',
              url: 'http://nested/bookmark/',
              dateAdded: 1,
              dateLastUsed: 10,
            },
          ],
        },
        {
          id: '4',
          parentId: '2',
          title: 'Child folder',
          dateAdded: 2,
          children: [
            {
              id: '8',
              parentId: '4',
              title: 'Nested bookmark',
              url: 'http://nested/bookmark/',
              dateAdded: 5,
              dateLastUsed: 6,
            },
          ],
        },
        {
          id: '5',
          parentId: '2',
          title: 'Child folder',
          dateAdded: 6,
          children: [
            {
              id: '10',
              parentId: '5',
              title: 'Child folder',
              dateAdded: 8,
              children: [
                {
                  id: '13',
                  parentId: '10',
                  title: 'Nested folder',
                  dateAdded: 0,
                  children: [],
                },
              ],
            },
          ],
        },
        {
          id: '6',
          parentId: '2',
          title: 'Child folder',
          dateAdded: 3,
          children: [
            {
              id: '12',
              parentId: '6',
              title: 'Child folder',
              dateAdded: 3,
              children: [
                {
                  id: '14',
                  parentId: '12',
                  title: 'Nested bookmark',
                  url: 'http://nested/bookmark/',
                  dateAdded: 9,
                  dateLastUsed: 1,
                },
              ],
            },
          ],
        },
      ],
    },
  ];

  setup(async () => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    bookmarksApi = new TestBookmarksApiProxy();
    bookmarksApi.setFolders(structuredClone(folders));
    BookmarksApiProxyImpl.setInstance(bookmarksApi);

    shoppingServiceApi = new TestShoppingServiceApiProxy();
    ShoppingServiceApiProxyImpl.setInstance(shoppingServiceApi);

    const pluralString = new TestPluralStringProxy();
    PluralStringProxyImpl.setInstance(pluralString);

    imageServiceHandler = TestMock.fromClass(PageImageServiceHandlerRemote);
    PageImageServiceBrowserProxy.setInstance(
        new PageImageServiceBrowserProxy(imageServiceHandler));
    imageServiceHandler.setResultFor('getPageImageUrl', Promise.resolve({
      result: {imageUrl: {url: 'https://example.com/image.png'}},
    }));

    loadTimeData.overrideValues({
      urlImagesEnabled: true,
    });

    delegate = new ServiceTestPowerBookmarksDelegate();
    service = new PowerBookmarksService(delegate);
    service.startListening();

    await delegate.whenCalled('onBookmarksLoaded');
  });

  test('FiltersTopLevelBookmarks', () => {
    const defaultBookmarks =
        service.filterBookmarks(undefined, 0, undefined, []);
    assertEquals(folders[0]!.children!.length, defaultBookmarks.length);
  });

  test('FiltersByFolder', () => {
    const folder = service.findBookmarkWithId('5');
    const folderBookmarks = service.filterBookmarks(folder, 0, undefined, []);
    assertEquals(
        folders[0]!.children![2]!.children!.length, folderBookmarks.length);
  });

  test('FiltersBySearchQuery', () => {
    const searchBookmarks = service.filterBookmarks(undefined, 0, 'http', []);
    assertEquals(searchBookmarks.length, 3);
  });

  test('FiltersByFolderAndSearchQuery', () => {
    const folder = service.findBookmarkWithId('5');
    const primaryList = service.filterBookmarks(folder, 0, 'http', []);
    const secondaryList =
        service.filterBookmarks(undefined, 0, 'http', [], folder);
    assertEquals(primaryList.length, 1);
    assertEquals(secondaryList.length, 2);
  });

  test('FiltersByPriceTracking', () => {
    const searchBookmarks = service.filterBookmarks(
        undefined, 0, undefined,
        [{label: 'Price Tracking', icon: '', active: true}]);
    assertEquals(searchBookmarks.length, 1);
    assertEquals(searchBookmarks[0]!.id, '3');
  });

  test('SortsByNewest', () => {
    const sortedBookmarks =
        service.filterBookmarks(undefined, 0, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '4');
    assertEquals(sortedBookmarks[2]!.id, '3');
  });

  test('SortsByNewestWithComplexDescendants', async () => {
    bookmarksApi.setFolders(structuredClone(complexFolders));
    service.startListening();

    await delegate.whenCalled('onBookmarksLoaded');

    const sortedBookmarks =
        service.filterBookmarks(undefined, 0, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '6');
    assertEquals(sortedBookmarks[1]!.id, '5');
    assertEquals(sortedBookmarks[2]!.id, '4');
    assertEquals(sortedBookmarks[3]!.id, '3');
  });

  test('SortsByOldest', () => {
    const sortedBookmarks =
        service.filterBookmarks(undefined, 1, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '3');
    assertEquals(sortedBookmarks[2]!.id, '4');
  });

  test('SortsByOldestWithComplexDescendants', async () => {
    bookmarksApi.setFolders(structuredClone(complexFolders));
    service.startListening();

    await delegate.whenCalled('onBookmarksLoaded');

    const sortedBookmarks =
        service.filterBookmarks(undefined, 1, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '3');
    assertEquals(sortedBookmarks[2]!.id, '4');
    assertEquals(sortedBookmarks[3]!.id, '6');
  });

  test('SortsByLastOpened', () => {
    const sortedBookmarks =
        service.filterBookmarks(undefined, 2, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '3');
    assertEquals(sortedBookmarks[2]!.id, '4');
  });

  test('SortsByLastOpenedWithComplexDescendants', async () => {
    bookmarksApi.setFolders(structuredClone(complexFolders));
    service.startListening();

    await delegate.whenCalled('onBookmarksLoaded');

    const sortedBookmarks =
        service.filterBookmarks(undefined, 2, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '3');
    assertEquals(sortedBookmarks[1]!.id, '5');
    assertEquals(sortedBookmarks[2]!.id, '4');
    assertEquals(sortedBookmarks[3]!.id, '6');
  });

  test('SortsByAToZ', () => {
    const sortedBookmarks =
        service.filterBookmarks(undefined, 3, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '3');
    assertEquals(sortedBookmarks[2]!.id, '4');
  });

  test('SortsByZToA', () => {
    const sortedBookmarks =
        service.filterBookmarks(undefined, 4, undefined, []);
    assertEquals(sortedBookmarks[0]!.id, '5');
    assertEquals(sortedBookmarks[1]!.id, '4');
    assertEquals(sortedBookmarks[2]!.id, '3');
  });

  test('CallsOnBookmarkChanged', () => {
    const changedBookmark = folders[0]!.children![0]!;
    bookmarksApi.callbackRouter.onChanged.callListeners(changedBookmark.id, {
      title: 'New title',
      url: 'http://new/url',
    });

    assertEquals(delegate.getCallCount('onBookmarkChanged'), 1);
  });

  test('CallsOnBookmarkCreated', async () => {
    bookmarksApi.callbackRouter.onCreated.callListeners('999', {
      id: '999',
      title: 'New bookmark',
      index: 0,
      parentId: folders[0]!.id,
      url: 'http://new/bookmark',
    });

    assertEquals(delegate.getCallCount('onBookmarkCreated'), 1);
  });

  test('CallsOnBookmarkMoved', () => {
    const movedBookmark = folders[0]!.children![2]!.children![0]!;
    bookmarksApi.callbackRouter.onMoved.callListeners(movedBookmark.id, {
      index: 0,
      parentId: folders[0]!.id,                   // Moving to other bookmarks.
      oldParentId: folders[0]!.children![2]!.id,  // Moving from child folder.
      oldIndex: 0,
    });

    assertEquals(delegate.getCallCount('onBookmarkMoved'), 1);
  });

  test('CallsOnBookmarkRemoved', () => {
    bookmarksApi.callbackRouter.onRemoved.callListeners('4');

    assertEquals(delegate.getCallCount('onBookmarkRemoved'), 1);
  });

  test('FindsBookmarkWithId', () => {
    const bookmarkWithValidId = service.findBookmarkWithId('6');
    assertEquals(bookmarkWithValidId!.id, '6');

    const bookmarkWithInvalidId = service.findBookmarkWithId('100');
    assertEquals(bookmarkWithInvalidId, undefined);
  });

  test('CanAddUrl', () => {
    const folder = service.findBookmarkWithId('2');
    assertEquals(service.canAddUrl('http://new/url/', folder), true);
    assertEquals(service.canAddUrl('http://child/bookmark/1/', folder), false);
    assertEquals(service.canAddUrl('http://nested/bookmark/', folder), true);
  });

  test('RequestsImages', async () => {
    assertEquals(imageServiceHandler.getCallCount('getPageImageUrl'), 0);

    service.setMaxImageServiceRequestsForTesting(2);
    service.refreshDataForBookmarks([
      service.findBookmarkWithId('3')!,
      service.findBookmarkWithId('4')!,
      service.findBookmarkWithId('6')!,
    ]);

    assertEquals(imageServiceHandler.getCallCount('getPageImageUrl'), 2);
    await flushTasks();
    assertEquals(imageServiceHandler.getCallCount('getPageImageUrl'), 3);
  });
});