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

// Copyright 2019 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 {BookmarksItemElement, BookmarksListElement} from 'chrome://bookmarks/bookmarks.js';
import {BookmarkManagerApiProxyImpl, Command} from 'chrome://bookmarks/bookmarks.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertDeepEquals, assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyDownOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import type {ModifiersParam} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';

import {TestBookmarkManagerApiProxy} from './test_bookmark_manager_api_proxy.js';
import {TestCommandManager} from './test_command_manager.js';
import {TestStore} from './test_store.js';
import {createFolder, createItem, getAllFoldersOpenState, normalizeIterable, replaceBody, testTree} from './test_util.js';

suite('<bookmarks-list>', function() {
  let list: BookmarksListElement;
  let store: TestStore;
  let items: NodeListOf<BookmarksItemElement>;
  let testCommandManager: TestCommandManager;
  const multiKey = isMac ? 'meta' : 'ctrl';

  function keydown(item: HTMLElement, key: string, modifiers?: ModifiersParam) {
    keyDownOn(item, 0, modifiers, key);
  }

  function getItem(id: string): BookmarksItemElement {
    const item = Array.from(items).find(({itemId}) => itemId === id);
    assertTrue(!!item, `Item ${id} does not exist in items.`);
    return item;
  }

  function selectAndFocus(id: string) {
    getItem(id).focus();
    store.data.selection.items = new Set([id]);
    store.notifyObservers();
  }

  function updateIds(ids: string[]) {
    store.data.nodes[store.data.selectedFolder]!.children = ids;
    store.notifyObservers();
  }

  function checkMenuButtonFocus(id: string) {
    assertEquals(getItem(id).$.menuButton, getDeepActiveElement());
  }

  async function doAndWait(fn: () => void) {
    fn();
    await flushTasks();
    // Focus is done asynchronously.
    await flushTasks();
  }

  setup(function() {
    const nodes = testTree(createFolder('1', [
      createItem('2'),
      createItem('3'),
      createItem('4'),
      createItem('5'),
      createItem('6'),
      createFolder('7', []),
    ]));
    store = new TestStore({
      nodes: nodes,
      folderOpenState: getAllFoldersOpenState(nodes),
      selectedFolder: '1',
    });
    store.setReducersEnabled(true);
    store.replaceSingleton();

    const proxy = new TestBookmarkManagerApiProxy();
    BookmarkManagerApiProxyImpl.setInstance(proxy);

    list = document.createElement('bookmarks-list');
    list.style.height = '100%';
    list.style.width = '100%';
    list.style.position = 'absolute';
    replaceBody(list);
    flush();
    items = list.shadowRoot!.querySelectorAll('bookmarks-item');

    testCommandManager = new TestCommandManager();
    document.body.appendChild(testCommandManager.getCommandManager());

    const toastManager = document.createElement('cr-toast-manager');
    document.body.appendChild(toastManager);

    return flushTasks();
  });

  test('simple keyboard selection', function() {
    assertEquals(6, items.length);

    let focusedItem = items[0]!;
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertEquals(0, focusedItem.$.menuButton.tabIndex);
    focusedItem.focus();

    keydown(focusedItem, 'ArrowDown');
    focusedItem = items[1]!;
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertEquals(0, focusedItem.$.menuButton.tabIndex);
    assertDeepEquals(['3'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowUp');
    focusedItem = items[0]!;
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowRight');
    focusedItem = items[0]!;
    assertEquals(
        focusedItem, document.activeElement!.shadowRoot!.activeElement);
    assertEquals(focusedItem.$.menuButton, items[0]!.shadowRoot!.activeElement);

    keydown(focusedItem, 'ArrowLeft');
    focusedItem = items[0]!;
    assertEquals(
        focusedItem, document.activeElement!.shadowRoot!.activeElement);
    assertEquals(null, items[0]!.shadowRoot!.activeElement);

    keydown(focusedItem, 'End');
    focusedItem = items[5]!;
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertDeepEquals(['7'], normalizeIterable(store.data.selection.items));

    // Moving past the end of the list is a no-op.
    keydown(focusedItem, 'ArrowDown');
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertDeepEquals(['7'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'Home');
    focusedItem = items[0]!;
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    // Moving past the start of the list is a no-op.
    keydown(focusedItem, 'ArrowUp');
    assertEquals('0', focusedItem.getAttribute('tabindex'));
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'Escape');
    assertDeepEquals([], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'a', multiKey);
    assertDeepEquals(
        ['2', '3', '4', '5', '6', '7'],
        normalizeIterable(store.data.selection.items));
  });

  test('shift selection', function() {
    assertEquals(6, items.length);

    let focusedItem = items[0]!;
    focusedItem.focus();

    keydown(focusedItem, 'ArrowDown', 'shift');
    focusedItem = items[1]!;
    assertDeepEquals(['2', '3'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'Escape');
    focusedItem = items[1]!;
    assertDeepEquals([], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowUp', 'shift');
    focusedItem = items[0]!;
    assertDeepEquals(['2', '3'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', 'shift');
    focusedItem = items[1]!;
    assertDeepEquals(['3'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', 'shift');
    focusedItem = items[2]!;
    assertDeepEquals(['3', '4'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'End', 'shift');
    focusedItem = items[2]!;
    assertDeepEquals(
        ['3', '4', '5', '6', '7'],
        normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'Home', 'shift');
    focusedItem = items[2]!;
    assertDeepEquals(['2', '3'], normalizeIterable(store.data.selection.items));
  });

  test('ctrl selection', function() {
    assertEquals(6, items.length);

    let focusedItem = items[0]!;
    focusedItem.focus();

    keydown(focusedItem, ' ', multiKey);
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', multiKey);
    focusedItem = items[1]!;
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));
    assertEquals('3', store.data.selection.anchor);

    keydown(focusedItem, 'ArrowDown', multiKey);
    focusedItem = items[2]!;
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, ' ', multiKey);
    assertDeepEquals(['2', '4'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, ' ', multiKey);
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));
  });

  test('ctrl+shift selection', function() {
    assertEquals(6, items.length);

    let focusedItem = items[0]!;
    focusedItem.focus();

    keydown(focusedItem, ' ', multiKey);
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', multiKey);
    focusedItem = items[1]!;
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', multiKey);
    focusedItem = items[2]!;
    assertDeepEquals(['2'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', [multiKey, 'shift']);
    focusedItem = items[3]!;
    assertDeepEquals(
        ['2', '4', '5'], normalizeIterable(store.data.selection.items));

    keydown(focusedItem, 'ArrowDown', [multiKey, 'shift']);
    focusedItem = items[3]!;
    assertDeepEquals(
        ['2', '4', '5', '6'], normalizeIterable(store.data.selection.items));
  });

  test('keyboard commands are passed to command manager', function() {
    store.data.selection.items = new Set(['2', '3']);
    store.notifyObservers();

    const focusedItem = items[4]!;
    focusedItem.focus();

    keydown(focusedItem, 'Delete');
    // Commands should take affect on the selection, even if something else is
    // focused.
    testCommandManager.assertLastCommand(Command.DELETE, ['2', '3']);
  });

  test('iron-list does not steal focus on enter', () => {
    assertTrue(!!items[0]);

    // Iron-list attempts to focus the whole <bookmarks-item> when pressing
    // enter on the menu button. This checks that we block this behavior
    // during keydown on <bookmarks-list>.
    const button = items[0].$.menuButton;
    button.focus();
    keydown(button, 'Enter');
    testCommandManager.getCommandManager().closeCommandMenu();
    assertEquals(button, items[0].shadowRoot!.activeElement);
  });

  test('remove first item, focus on first item', async () => {
    await doAndWait(() => {
      selectAndFocus('2');
      updateIds(['3', '4', '5', '6', '7']);
    });
    checkMenuButtonFocus('3');
  });

  test('remove last item, focus on last item', async () => {
    await doAndWait(() => {
      selectAndFocus('7');
      updateIds(['2', '3', '4', '5', '6']);
    });
    checkMenuButtonFocus('6');
  });

  test('remove middle item, focus on item with same index', async () => {
    await doAndWait(() => {
      selectAndFocus('3');
      updateIds(['2', '4', '5', '6', '7']);
    });
    checkMenuButtonFocus('4');
  });

  test('reorder items, focus does not change', async () => {
    await doAndWait(() => {
      selectAndFocus('3');
      updateIds(['2', '4', '5', '6', '3', '7']);
    });
    assertEquals(document.body, getDeepActiveElement());
  });
});