chromium/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_view_test.ts

// Copyright 2021 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://shortcut-customization/js/accelerator_edit_view.js';
import 'chrome://webui-test/chromeos/mojo_webui_test_support.js';

import {VKey} from 'chrome://resources/ash/common/shortcut_input_ui/accelerator_keys.mojom-webui.js';
import {FakeShortcutInputProvider} from 'chrome://resources/ash/common/shortcut_input_ui/fake_shortcut_input_provider.js';
import {KeyEvent} from 'chrome://resources/ash/common/shortcut_input_ui/input_device_settings.mojom-webui.js';
import {Modifier as ModifierEnum} from 'chrome://resources/ash/common/shortcut_input_ui/shortcut_utils.js';
import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {AcceleratorEditViewElement} from 'chrome://shortcut-customization/js/accelerator_edit_view.js';
import {AcceleratorLookupManager} from 'chrome://shortcut-customization/js/accelerator_lookup_manager.js';
import {fakeAcceleratorConfig, fakeDefaultAccelerators, fakeLayoutInfo} from 'chrome://shortcut-customization/js/fake_data.js';
import {FakeShortcutProvider} from 'chrome://shortcut-customization/js/fake_shortcut_provider.js';
import {setShortcutProviderForTesting} from 'chrome://shortcut-customization/js/mojo_interface_provider.js';
import {setShortcutInputProviderForTesting} from 'chrome://shortcut-customization/js/shortcut_input_mojo_interface_provider.js';
import {AcceleratorConfigResult, AcceleratorSource, Modifier} from 'chrome://shortcut-customization/js/shortcut_types.js';
import {AcceleratorResultData, Subactions} from 'chrome://shortcut-customization/mojom-webui/shortcut_customization.mojom-webui.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {createStandardAcceleratorInfo, createUserAcceleratorInfo} from './shortcut_customization_test_util.js';

suite('acceleratorEditViewTest', function() {
  let editViewElement: AcceleratorEditViewElement|null = null;
  let manager: AcceleratorLookupManager|null = null;
  let provider: FakeShortcutProvider;
  const shortcutInputProvider: FakeShortcutInputProvider =
      new FakeShortcutInputProvider();

  setup(() => {
    provider = new FakeShortcutProvider();
    provider.setFakeGetDefaultAcceleratorsForId(fakeDefaultAccelerators);
    setShortcutProviderForTesting(provider);
    setShortcutInputProviderForTesting(shortcutInputProvider);

    manager = AcceleratorLookupManager.getInstance();
    manager.setAcceleratorLookup(fakeAcceleratorConfig);
    manager.setAcceleratorLayoutLookup(fakeLayoutInfo);

    editViewElement = document.createElement('accelerator-edit-view');
    document.body.appendChild(editViewElement);
  });

  teardown(() => {
    if (manager) {
      manager.reset();
    }
    editViewElement!.remove();
    editViewElement = null;
  });

  function getElementById(id: string): HTMLElement {
    assertTrue(!!editViewElement);
    const element = editViewElement.shadowRoot!.getElementById(id);
    assertTrue(!!element);
    return element as HTMLElement;
  }

  test('LoadsBasicEditView', async () => {
    const acceleratorInfo = createUserAcceleratorInfo(
        Modifier.CONTROL | Modifier.SHIFT,
        /*key=*/ 71,
        /*keyDisplay=*/ 'g');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    await flush();

    // Check that the edit buttons are visible.
    assertTrue(isVisible(getElementById('editButtonsContainer')));
    assertFalse(isVisible(getElementById('cancelButtonContainer')));

    // Verify that no metrics were sent.
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(undefined, provider.getLastRecordedSubactions());

    // Click on the edit button.
    getElementById('editButton')!.click();

    // Only the Cancel button should now be visible.
    assertFalse(isVisible(getElementById('editButtonsContainer')));
    assertTrue(isVisible(getElementById('cancelButtonContainer')));

    // Click on the Cancel button and expect the edit buttons to be available.
    getElementById('cancelButton')!.click();

    await flushTasks();
    assertTrue(isVisible(getElementById('editButtonsContainer')));
    assertFalse(isVisible(getElementById('cancelButtonContainer')));

    // Expect metric recorded.
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(
        Subactions.kNoErrorCancel, provider.getLastRecordedSubactions());
  });

  test('SuccessfulEdit', async () => {
    const acceleratorInfo = createUserAcceleratorInfo(
        Modifier.CONTROL | Modifier.SHIFT,
        /*key=*/ 71,
        /*keyDisplay=*/ 'g');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    await flush();

    // Check that the edit buttons are visible.
    assertTrue(isVisible(getElementById('editButtonsContainer')));
    assertFalse(isVisible(getElementById('cancelButtonContainer')));

    // Verify that no metrics were sent.
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(undefined, provider.getLastRecordedSubactions());

    // Click on the edit button.
    getElementById('editButton')!.click();
    await flushTasks();

    const fakeResult: AcceleratorResultData = {
      result: AcceleratorConfigResult.kSuccess,
      shortcutName: null,
    };

    provider.setFakeReplaceAcceleratorResult(fakeResult);

    // Press another shortcut, expect no error.
    const keyEvent: KeyEvent = {
      vkey: VKey.kKeyE,
      domCode: 0,
      domKey: 0,
      modifiers: ModifierEnum.ALT | ModifierEnum.CONTROL,
      keyDisplay: 'e',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent, keyEvent);

    await flushTasks();
    assertFalse(editViewElement!.hasError);

    // Expect metric recorded.
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(
        Subactions.kNoErrorSuccess, provider.getLastRecordedSubactions());
  });

  test('DetectShortcutConflict', async () => {
    const fakeResult: AcceleratorResultData = {
      result: AcceleratorConfigResult.kConflict,
      shortcutName: {data: [1]},
    };

    provider.setFakeReplaceAcceleratorResult(fakeResult);

    const acceleratorInfo = createStandardAcceleratorInfo(
        Modifier.ALT,
        /*key=*/ 221,
        /*keyDisplay=*/ ']');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    editViewElement!.source = AcceleratorSource.kAsh;
    editViewElement!.action = 1;
    await flushTasks();

    // Check that the edit buttons are visible.
    assertTrue(isVisible(getElementById('editButtonsContainer')));
    assertFalse(isVisible(getElementById('cancelButtonContainer')));

    // Assert that no error has occurred.
    assertFalse(editViewElement!.hasError);
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(undefined, provider.getLastRecordedSubactions());

    // Click on the edit button.
    getElementById('editButton')!.click();
    await flushTasks();

    // Press 'Snap Window left' key, expect an error since it is a
    // pre-existing shortcut.
    const keyEvent: KeyEvent = {
      vkey: VKey.kOem4,
      domCode: 0,
      domKey: 0,
      modifiers: ModifierEnum.ALT | ModifierEnum.CONTROL,
      keyDisplay: 'BracketLeft',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent, keyEvent);

    await flushTasks();
    assertTrue(editViewElement!.hasError);

    const fakeResult2: AcceleratorResultData = {
      result: AcceleratorConfigResult.kSuccess,
      shortcutName: null,
    };

    provider.setFakeReplaceAcceleratorResult(fakeResult2);
    // Press another shortcut, expect no error.
    const keyEvent2: KeyEvent = {
      vkey: VKey.kKeyE,
      domCode: 0,
      domKey: 0,
      modifiers: ModifierEnum.ALT | ModifierEnum.CONTROL,
      keyDisplay: 'e',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent2, keyEvent2);

    await flushTasks();
    assertFalse(editViewElement!.hasError);

    // Now click on the cancel button and expect an error.
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(
        Subactions.kErrorSuccess, provider.getLastRecordedSubactions());
  });

  test('CancelError', async () => {
    const fakeResult: AcceleratorResultData = {
      result: AcceleratorConfigResult.kConflict,
      shortcutName: {data: [1]},
    };

    provider.setFakeReplaceAcceleratorResult(fakeResult);

    const acceleratorInfo = createStandardAcceleratorInfo(
        Modifier.ALT,
        /*key=*/ 221,
        /*keyDisplay=*/ ']');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    editViewElement!.source = AcceleratorSource.kAsh;
    editViewElement!.action = 1;
    await flushTasks();

    // Check that the edit buttons are visible.
    assertTrue(isVisible(getElementById('editButtonsContainer')));
    assertFalse(isVisible(getElementById('cancelButtonContainer')));

    // Assert that no error has occurred.
    assertFalse(editViewElement!.hasError);
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(undefined, provider.getLastRecordedSubactions());

    // Click on the edit button.
    getElementById('editButton')!.click();
    await flushTasks();

    // Press 'Snap Window left' key, expect an error since it is a
    // pre-existing shortcut.
    const keyEvent: KeyEvent = {
      vkey: VKey.kOem4,
      domCode: 0,
      domKey: 0,
      modifiers: ModifierEnum.ALT,
      keyDisplay: 'BracketLeft',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent, keyEvent);

    await flushTasks();
    assertTrue(editViewElement!.hasError);

    // Click the cancel button and expect metric to be fired.
    getElementById('cancelButton')!.click();
    assertFalse(provider.getLastRecordedIsAdd());
    assertEquals(Subactions.kErrorCancel, provider.getLastRecordedSubactions());
  });

  test('PressKeyToResetError', async () => {
    const fakeResult: AcceleratorResultData = {
      result: AcceleratorConfigResult.kConflict,
      shortcutName: {data: [1]},
    };

    provider.setFakeReplaceAcceleratorResult(fakeResult);

    const acceleratorInfo = createStandardAcceleratorInfo(
        Modifier.ALT,
        /*key=*/ 221,
        /*keyDisplay=*/ ']');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    editViewElement!.source = AcceleratorSource.kAsh;
    editViewElement!.action = 1;
    await flushTasks();

    // Click on the edit button.
    getElementById('editButton')!.click();

    await flushTasks();

    // Press 'Snap Window left' key, expect an error since it is a
    // pre-existing shortcut.
    const keyEvent: KeyEvent = {
      vkey: VKey.kOem4,
      domCode: 0,
      domKey: 0,
      modifiers: ModifierEnum.ALT,
      keyDisplay: 'BracketLeft',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent, keyEvent);

    await flushTasks();
    assertTrue(editViewElement!.hasError);

    // Press a single key, expect error to be reset.
    const keyEvent2: KeyEvent = {
      vkey: VKey.kKeyA,
      domCode: 0,
      domKey: 0,
      modifiers: 0,
      keyDisplay: 'a',
    };
    shortcutInputProvider.sendKeyPressEvent(keyEvent2, keyEvent2);

    await flushTasks();
    assertFalse(editViewElement!.hasError);
  });

  test('ClickEditAndShowInputHint', async () => {
    const acceleratorInfo = createStandardAcceleratorInfo(
        Modifier.ALT,
        /*key=*/ 221,
        /*keyDisplay=*/ ']');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    editViewElement!.source = AcceleratorSource.kAsh;
    editViewElement!.action = 1;
    await flushTasks();

    // Check that the edit button is visible.
    assertTrue(isVisible(getElementById('editButtonsContainer')));

    // Click on the edit button.
    getElementById('editButton')!.click();

    // Input hint message should be shown.
    const expectedHintMessage =
        'Press 1-4 modifiers and 1 other key on your keyboard. To exit ' +
        'editing mode, press alt + esc.';
    const statusMessageElement = strictQuery(
        '#container',
        editViewElement!.shadowRoot!.querySelector('#status')!.shadowRoot,
        HTMLDivElement);
    assertEquals(expectedHintMessage, statusMessageElement.textContent!.trim());
  });

  test('ClickCancelButton', async () => {
    const acceleratorInfo = createUserAcceleratorInfo(
        Modifier.CONTROL | Modifier.SHIFT,
        /*key=*/ 71,
        /*keyDisplay=*/ 'g');

    editViewElement!.acceleratorInfo = acceleratorInfo;
    await flush();

    // Click on the edit button.
    getElementById('editButton')!.click();

    // Expect cancel button is visible.
    const cancelButton = getElementById('cancelButton');
    assertTrue(isVisible(cancelButton));

    // Set up flags for event listeners
    let onBlurCalled = false;
    let onCancelButtonClickedCalled = false;

    // Attach event listeners
    editViewElement!.addEventListener('blur', () => {
      onBlurCalled = true;
    });
    cancelButton!.addEventListener('click', () => {
      onCancelButtonClickedCalled = true;
    });

    // Click on cancel button.
    cancelButton.click();
    await flush();

    // Expect blur event is not triggered.
    assertTrue(onCancelButtonClickedCalled);
    assertFalse(onBlurCalled);
  });
});