chromium/chrome/test/data/webui/bookmarks/reducers_test.ts

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

import type {BookmarksPageState, FolderOpenState, NodeMap, SelectFolderAction, SelectionState, SelectItemsAction} from 'chrome://bookmarks/bookmarks.js';
import {changeFolderOpen, clearSearch, createBookmark, createEmptyState, deselectItems, editBookmark, getDisplayedList, isShowingSearch, moveBookmark, reduceAction, removeBookmark, reorderChildren, selectFolder, setSearchResults, setSearchTerm, updateAnchor, updateFolderOpenState, updateNodes, updateSelectedFolder, updateSelection} from 'chrome://bookmarks/bookmarks.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';

import {createFolder, createItem, normalizeIterable, testTree} from './test_util.js';

suite('selection state', function() {
  let selection: SelectionState;
  let action;

  function select(
      items: string[], anchor: string, clear: boolean,
      toggle: boolean): SelectItemsAction {
    return {
      name: 'select-items',
      clear: clear,
      anchor: anchor,
      items: items,
      toggle: toggle,
    };
  }

  setup(function() {
    selection = {
      anchor: null,
      items: new Set(),
    };
  });

  test('can select an item', function() {
    action = select(['1'], '1', true, false);
    selection = updateSelection(selection, action);

    assertDeepEquals(['1'], normalizeIterable(selection.items));
    assertEquals('1', selection.anchor);

    // Replace current selection.
    action = select(['2'], '2', true, false);
    selection = updateSelection(selection, action);
    assertDeepEquals(['2'], normalizeIterable(selection.items));
    assertEquals('2', selection.anchor);

    // Add to current selection.
    action = select(['3'], '3', false, false);
    selection = updateSelection(selection, action);
    assertDeepEquals(['2', '3'], normalizeIterable(selection.items));
    assertEquals('3', selection.anchor);
  });

  test('can select multiple items', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);
    assertDeepEquals(['1', '2', '3'], normalizeIterable(selection.items));

    action = select(['3', '4'], '4', false, false);
    selection = updateSelection(selection, action);
    assertDeepEquals(['1', '2', '3', '4'], normalizeIterable(selection.items));
  });

  test('is cleared when selected folder changes', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);

    action = selectFolder('2');
    selection = updateSelection(selection, action!);
    assertDeepEquals(new Set(), selection.items);
  });

  test('is cleared when search finished', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);

    action = setSearchResults(['2']);
    selection = updateSelection(selection, action);
    assertDeepEquals(new Set(), selection.items);
  });

  test('is cleared when search cleared', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);

    action = clearSearch();
    selection = updateSelection(selection, action);
    assertDeepEquals(new Set(), selection.items);
  });

  test('deselect items', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);

    action = deselectItems();
    selection = updateSelection(selection, action);
    assertDeepEquals(new Set(), selection.items);
  });

  test('toggle an item', function() {
    action = select(['1', '2', '3'], '3', true, false);
    selection = updateSelection(selection, action);

    action = select(['1'], '3', false, true);
    selection = updateSelection(selection, action);
    assertDeepEquals(['2', '3'], normalizeIterable(selection.items));
  });

  test('update anchor', function() {
    action = updateAnchor('3');
    selection = updateSelection(selection, action);

    assertEquals('3', selection.anchor);
  });

  test('deselects items when they are deleted', function() {
    const nodeMap = testTree(
        createFolder(
            '1',
            [
              createItem('2'),
              createItem('3'),
              createItem('4'),
            ]),
        createItem('5'));

    action = select(['2', '4', '5'], '4', true, false);
    selection = updateSelection(selection, action);

    action = removeBookmark('1', '0', 0, nodeMap);
    selection = updateSelection(selection, action);

    assertDeepEquals(['5'], normalizeIterable(selection.items));
    assertEquals(null, selection.anchor);
  });

  test('deselects items when they are moved to a different folder', function() {
    action = select(['2', '3'], '2', true, false);
    selection = updateSelection(selection, action);

    // Move item '2' from the 1st item in '0' to the 0th item in '1'.
    action = moveBookmark('2', '1', 0, '0', 1);
    selection = updateSelection(selection, action);

    assertDeepEquals(['3'], normalizeIterable(selection.items));
    assertEquals(null, selection.anchor);
  });
});

suite('folder open state', function() {
  let nodes: NodeMap;
  let folderOpenState: FolderOpenState;
  let action;

  setup(function() {
    nodes = testTree(
        createFolder(
            '1',
            [
              createFolder('2', []),
              createItem('3'),
            ]),
        createFolder('4', []));
    folderOpenState = new Map();
  });

  test('close folder', function() {
    action = changeFolderOpen('2', false);
    folderOpenState = updateFolderOpenState(folderOpenState, action, nodes);
    assertFalse(folderOpenState.has('1'));
    assertFalse(folderOpenState.get('2')!);
  });

  test('select folder with closed parent', function() {
    // Close '1'
    action = changeFolderOpen('1', false);
    folderOpenState = updateFolderOpenState(folderOpenState, action, nodes);
    assertFalse(folderOpenState.get('1')!);
    assertFalse(folderOpenState.has('2'));

    // Should re-open when '2' is selected.
    action = selectFolder('2');
    folderOpenState = updateFolderOpenState(folderOpenState, action!, nodes);
    assertTrue(folderOpenState.get('1')!);
    assertFalse(folderOpenState.has('2'));

    // The parent should be set to permanently open, even if it wasn't
    // explicitly closed.
    folderOpenState = new Map();
    action = selectFolder('2');
    folderOpenState = updateFolderOpenState(folderOpenState, action!, nodes);
    assertTrue(folderOpenState.get('1')!);
    assertFalse(folderOpenState.has('2'));
  });

  test('move nodes in a closed folder', function() {
    // Moving bookmark items should not open folders.
    folderOpenState = new Map([['1', false]]);
    action = moveBookmark('3', '1', 1, '1', 0);
    folderOpenState = updateFolderOpenState(folderOpenState, action, nodes);

    assertFalse(folderOpenState.get('1')!);

    // Moving folders should open their parents.
    folderOpenState = new Map([['1', false], ['2', false]]);
    action = moveBookmark('4', '2', 0, '0', 1);
    folderOpenState = updateFolderOpenState(folderOpenState, action, nodes);
    assertTrue(folderOpenState.get('1')!);
    assertTrue(folderOpenState.get('2')!);
  });
});

suite('selected folder', function() {
  let nodes: NodeMap;
  let selectedFolder: string;
  let action: SelectFolderAction;

  setup(function() {
    nodes = testTree(createFolder('1', [
      createFolder(
          '2',
          [
            createFolder('3', []),
            createFolder('4', []),
          ]),
    ]));

    selectedFolder = '1';
  });

  test('updates from selectFolder action', function() {
    action = selectFolder('2')!;
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);
    assertEquals('2', selectedFolder);
  });

  test('updates when parent of selected folder is closed', function() {
    action = selectFolder('2')!;
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);

    action = changeFolderOpen('1', false);
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);
    assertEquals('1', selectedFolder);
  });

  test('selects ancestor when selected folder is deleted', function() {
    action = selectFolder('3')!;
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);

    // Delete the selected folder:
    action = removeBookmark('3', '2', 0, nodes);
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);

    assertEquals('2', selectedFolder);

    action = selectFolder('4')!;
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);

    // Delete an ancestor of the selected folder:
    action = removeBookmark('2', '1', 0, nodes);
    selectedFolder = updateSelectedFolder(selectedFolder, action, nodes);

    assertEquals('1', selectedFolder);
  });
});

suite('node state', function() {
  let nodes: NodeMap;
  let action;

  setup(function() {
    nodes = testTree(
        createFolder(
            '1',
            [
              createItem('2', {title: 'a', url: 'a.com'}),
              createItem('3'),
              createFolder('4', []),
            ]),
        createFolder('5', []));
  });

  test('updates when a node is edited', function() {
    action = editBookmark('2', {title: 'b'});
    nodes = updateNodes(nodes, action);

    assertEquals('b', nodes['2']!.title);
    assertEquals('a.com', nodes['2']!.url);

    action = editBookmark('2', {title: 'c', url: 'c.com'});
    nodes = updateNodes(nodes, action);

    assertEquals('c', nodes['2']!.title);
    assertEquals('c.com', nodes['2']!.url);

    action = editBookmark('4', {title: 'folder'});
    nodes = updateNodes(nodes, action);

    assertEquals('folder', nodes['4']!.title);
    assertEquals(undefined, nodes['4']!.url);

    // Cannot edit URL of a folder:
    action = editBookmark('4', {title: 'folder', url: 'folder.com'});
    nodes = updateNodes(nodes, action);

    assertEquals('folder', nodes['4']!.title);
    assertEquals(undefined, nodes['4']!.url);
  });

  test('updates when a node is created', function() {
    // Create a folder.
    const folder = {
      title: '',
      id: '6',
      parentId: '1',
      index: 2,
    };
    action = createBookmark(folder.id, folder);
    nodes = updateNodes(nodes, action);

    assertEquals('1', nodes['6']!.parentId);
    assertDeepEquals([], nodes['6']!.children);
    assertDeepEquals(['2', '3', '6', '4'], nodes['1']!.children);

    // Add a new item to that folder.
    const item = {
      title: '',
      id: '7',
      parentId: '6',
      index: 0,
      url: 'https://www.example.com',
    };

    action = createBookmark(item.id, item);
    nodes = updateNodes(nodes, action);

    assertEquals('6', nodes['7']!.parentId);
    assertEquals(undefined, nodes['7']!.children);
    assertDeepEquals(['7'], nodes['6']!.children);
  });

  test('updates when a node is deleted', function() {
    action = removeBookmark('3', '1', 1, nodes);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['2', '4'], nodes['1']!.children);

    assertDeepEquals(['2', '4'], nodes['1']!.children);
    assertEquals(undefined, nodes['3']);
  });

  test('removes all children of deleted nodes', function() {
    action = removeBookmark('1', '0', 0, nodes);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['0', '5'], Object.keys(nodes).sort());
  });

  test('updates when a node is moved', function() {
    // Move within the same folder backwards.
    action = moveBookmark('3', '1', 0, '1', 1);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['3', '2', '4'], nodes['1']!.children);

    // Move within the same folder forwards.
    action = moveBookmark('3', '1', 2, '1', 0);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['2', '4', '3'], nodes['1']!.children);

    // Move between different folders.
    action = moveBookmark('4', '5', 0, '1', 1);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['2', '3'], nodes['1']!.children);
    assertDeepEquals(['4'], nodes['5']!.children);
  });

  test('updates when children of a node are reordered', function() {
    action = reorderChildren('1', ['4', '2', '3']);
    nodes = updateNodes(nodes, action);

    assertDeepEquals(['4', '2', '3'], nodes['1']!.children);
  });
});

suite('search state', function() {
  let state: BookmarksPageState;

  setup(function() {
    // Search touches a few different things, so we test using the entire state.
    state = createEmptyState();
    state.nodes = testTree(createFolder('1', [
      createFolder(
          '2',
          [
            createItem('3'),
          ]),
    ]));
  });

  test('updates when search is started and finished', function() {
    let action;

    action = selectFolder('2');
    state = reduceAction(state, action!);

    action = setSearchTerm('test');
    state = reduceAction(state, action!);

    assertEquals('test', state.search.term);
    assertTrue(state.search.inProgress);

    // UI should not have changed yet.
    assertFalse(isShowingSearch(state));
    assertDeepEquals(['3'], getDisplayedList(state));

    action = setSearchResults(['2', '3']);
    const searchedState = reduceAction(state, action);

    assertFalse(searchedState.search.inProgress);

    // UI changes once search results arrive.
    assertTrue(isShowingSearch(searchedState));
    assertDeepEquals(['2', '3'], getDisplayedList(searchedState));

    // Case 1: Clear search by setting an empty search term.
    action = setSearchTerm('');
    const clearedState = reduceAction(searchedState, action);

    // Should go back to displaying the contents of '2', which was shown before
    // the search.
    assertEquals('2', clearedState.selectedFolder);
    assertFalse(isShowingSearch(clearedState));
    assertDeepEquals(['3'], getDisplayedList(clearedState));
    assertEquals('', clearedState.search.term);
    assertDeepEquals(null, clearedState.search.results);

    // Case 2: Clear search by selecting a new folder.
    action = selectFolder('1');
    const selectedState = reduceAction(searchedState, action!);

    assertEquals('1', selectedState.selectedFolder);
    assertFalse(isShowingSearch(selectedState));
    assertDeepEquals(['2'], getDisplayedList(selectedState));
    assertEquals('', selectedState.search.term);
    assertDeepEquals(null, selectedState.search.results);
  });

  test('results do not clear while performing a second search', function() {
    let action = setSearchTerm('te');
    state = reduceAction(state, action);

    assertFalse(isShowingSearch(state));

    action = setSearchResults(['2', '3']);
    state = reduceAction(state, action);

    assertFalse(state.search.inProgress);
    assertTrue(isShowingSearch(state));

    // Continuing the search should not clear the previous results, which should
    // continue to show until the new results arrive.
    action = setSearchTerm('test');
    state = reduceAction(state, action);

    assertTrue(state.search.inProgress);
    assertTrue(isShowingSearch(state));
    assertDeepEquals(['2', '3'], getDisplayedList(state));

    action = setSearchResults(['3']);
    state = reduceAction(state, action);

    assertFalse(state.search.inProgress);
    assertTrue(isShowingSearch(state));
    assertDeepEquals(['3'], getDisplayedList(state));
  });

  test('removes deleted nodes', function() {
    let action;

    action = setSearchTerm('test');
    state = reduceAction(state, action);

    action = setSearchResults(['1', '3', '2']);
    state = reduceAction(state, action);

    action = removeBookmark('2', '1', 0, state.nodes);
    state = reduceAction(state, action);

    // 2 and 3 should be removed, since 2 was deleted and 3 was a descendant of
    // 2.
    assertDeepEquals(['1'], state.search.results);
  });
});