chromium/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_card_test.ts

// Copyright 2024 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://os-settings/lazy_load.js';

import {AddDialogPage, FACEGAZE_COMMAND_PAIR_ADDED_EVENT_NAME, FaceGazeActionsCardElement, FaceGazeAddActionDialogElement, FaceGazeCommandPair} from 'chrome://os-settings/lazy_load.js';
import {CrButtonElement, CrIconButtonElement, CrSettingsPrefs, Router, routes, SettingsPrefsElement, SettingsToggleButtonElement} from 'chrome://os-settings/os_settings.js';
import {FacialGesture} from 'chrome://resources/ash/common/accessibility/facial_gestures.js';
import {MacroName} from 'chrome://resources/ash/common/accessibility/macro_names.js';
import {assert} from 'chrome://resources/js/assert.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNull, 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 {clearBody} from '../utils.js';

suite('<facegaze-actions-card>', () => {
  function getDialog(): FaceGazeAddActionDialogElement {
    const dialog =
        faceGazeActionsCard.shadowRoot!
            .querySelector<FaceGazeAddActionDialogElement>('#actionsAddDialog');
    assertTrue(!!dialog);
    return dialog;
  }

  function getAddButton(): CrButtonElement {
    return getButton('#addActionButton');
  }

  function getButton(id: string): CrButtonElement {
    const button =
        faceGazeActionsCard.shadowRoot!.querySelector<CrButtonElement>(id);
    assertTrue(!!button);
    assertTrue(isVisible(button));
    return button;
  }

  let faceGazeActionsCard: FaceGazeActionsCardElement;
  let prefElement: SettingsPrefsElement;

  function isCommandPairSetInPrefs(
      expectedMacro: MacroName, expectedGesture: FacialGesture): boolean {
    const assignedGestures = {...faceGazeActionsCard.prefs.settings.a11y
                                  .face_gaze.gestures_to_macros.value};
    for (const [currentGesture, assignedMacro] of Object.entries(
             assignedGestures)) {
      if (expectedGesture === currentGesture &&
          expectedMacro === assignedMacro) {
        return true;
      }
    }

    return false;
  }

  async function fireCommandPairAddedEvent(
      macro: MacroName, gesture: FacialGesture|null) {
    getAddButton().click();
    await flushTasks();

    const dialog = getDialog();

    const commandPair = new FaceGazeCommandPair(macro, gesture);
    const event = new CustomEvent(FACEGAZE_COMMAND_PAIR_ADDED_EVENT_NAME, {
      bubbles: true,
      composed: true,
      detail: commandPair,
    });

    dialog.dispatchEvent(event);
  }

  async function initPage() {
    prefElement = document.createElement('settings-prefs');
    document.body.appendChild(prefElement);

    await CrSettingsPrefs.initialized;
    faceGazeActionsCard = document.createElement('facegaze-actions-card');
    faceGazeActionsCard.prefs = prefElement.prefs;
    document.body.appendChild(faceGazeActionsCard);
    flush();
  }

  setup(() => {
    clearBody();
    Router.getInstance().navigateTo(routes.MANAGE_FACEGAZE_SETTINGS);
  });

  teardown(() => {
    faceGazeActionsCard.remove();
    prefElement.remove();
    Router.getInstance().resetRouteForTesting();
  });

  test('actions enabled button syncs to pref', async () => {
    await initPage();
    assertTrue(faceGazeActionsCard.prefs.settings.a11y.face_gaze.actions_enabled
                   .value);

    const button = faceGazeActionsCard.shadowRoot!
                       .querySelector<SettingsToggleButtonElement>(
                           '#faceGazeActionsEnabledButton');
    assert(button);
    assertTrue(isVisible(button));
    assertTrue(button.checked);

    button.click();
    flush();

    assertFalse(button.checked);
    assertFalse(faceGazeActionsCard.prefs.settings.a11y.face_gaze
                    .actions_enabled.value);
  });

  test('actions disables controls if feature is disabled', async () => {
    await initPage();

    faceGazeActionsCard.disabled = true;
    await flushTasks();

    const addButton = getAddButton();
    assertTrue(addButton.disabled);

    faceGazeActionsCard.disabled = false;
    await flushTasks();
    assertFalse(addButton.disabled);
  });

  test(
      'actions disables configuration controls if toggle is turned off',
      async () => {
        await initPage();

        faceGazeActionsCard.set(
            'prefs.settings.a11y.face_gaze.actions_enabled.value', true);
        await flushTasks();

        const addButton = getAddButton();
        assertFalse(addButton.disabled);

        faceGazeActionsCard.set(
            'prefs.settings.a11y.face_gaze.actions_enabled.value', false);
        await flushTasks();

        assertTrue(addButton.disabled);
      });

  test('actions initializes command pairs from prefs', async () => {
    prefElement = document.createElement('settings-prefs');
    document.body.appendChild(prefElement);

    await CrSettingsPrefs.initialized;
    faceGazeActionsCard = document.createElement('facegaze-actions-card');
    faceGazeActionsCard.prefs = prefElement.prefs;

    const expectedMacro: MacroName = MacroName.MOUSE_CLICK_LEFT;
    const expectedGesture: FacialGesture = FacialGesture.EYES_BLINK;
    faceGazeActionsCard.prefs.settings.a11y.face_gaze.gestures_to_macros
        .value[expectedGesture] = expectedMacro;

    document.body.appendChild(faceGazeActionsCard);
    flush();

    assertTrue(isCommandPairSetInPrefs(expectedMacro, expectedGesture));

    const commandPairs = faceGazeActionsCard.get(
        FaceGazeActionsCardElement.FACEGAZE_COMMAND_PAIRS_PROPERTY_NAME);
    assertEquals(1, commandPairs.length);
  });

  test('actions update prefs with added command pair', async () => {
    await initPage();

    const expectedMacro: MacroName = MacroName.MOUSE_CLICK_LEFT;
    const expectedGesture: FacialGesture = FacialGesture.EYES_BLINK;
    assertFalse(isCommandPairSetInPrefs(expectedMacro, expectedGesture));
    await fireCommandPairAddedEvent(expectedMacro, expectedGesture);
    assertTrue(isCommandPairSetInPrefs(expectedMacro, expectedGesture));
  });

  test('actions update prefs based on removed command pair', async () => {
    await initPage();

    const expectedMacro: MacroName = MacroName.MOUSE_CLICK_LEFT;
    const expectedGesture: FacialGesture = FacialGesture.EYES_BLINK;
    await fireCommandPairAddedEvent(expectedMacro, expectedGesture);
    assertTrue(isCommandPairSetInPrefs(expectedMacro, expectedGesture));
    flush();

    const removeButton =
        faceGazeActionsCard.shadowRoot!.querySelector<CrIconButtonElement>(
            '.icon-clear');
    assertTrue(!!removeButton);
    removeButton.click();
    await flushTasks();

    assertFalse(isCommandPairSetInPrefs(expectedMacro, expectedGesture));
  });

  test('actions add button opens dialog on action page', async () => {
    await initPage();

    getAddButton().click();
    await flushTasks();

    const dialog = getDialog();
    assertEquals(AddDialogPage.SELECT_ACTION, dialog.getCurrentPageForTest());
    assertNull(dialog.actionToAssignGesture);
  });

  test(
      'actions assign gesture button opens dialog on gesture page',
      async () => {
        await initPage();

        await fireCommandPairAddedEvent(MacroName.MOUSE_CLICK_LEFT, null);
        flush();

        const chip = faceGazeActionsCard.shadowRoot!.querySelector('cros-chip');
        assertTrue(!!chip);
        chip.click();
        await flushTasks();

        const dialog = getDialog();
        assertEquals(AddDialogPage.SELECT_GESTURE, dialog.initialPage);
        assertTrue(!!dialog.actionToAssignGesture);
      });

  test('actions gesture button opens dialog on gesture page', async () => {
    await initPage();

    await fireCommandPairAddedEvent(
        MacroName.MOUSE_CLICK_LEFT, FacialGesture.BROWS_DOWN);
    flush();

    const chip = faceGazeActionsCard.shadowRoot!.querySelector('cros-chip');
    assertTrue(!!chip);
    chip.click();
    await flushTasks();

    const dialog = getDialog();
    assertEquals(AddDialogPage.GESTURE_THRESHOLD, dialog.initialPage);
    assertTrue(!!dialog.gestureToConfigure);
  });

  test('actions dialog left click gestures is updated', async () => {
    await initPage();

    await fireCommandPairAddedEvent(
        MacroName.MOUSE_CLICK_LEFT, FacialGesture.EYES_BLINK);
    flush();

    getAddButton().click();
    await flushTasks();

    const dialog = getDialog();
    assertEquals(AddDialogPage.SELECT_ACTION, dialog.initialPage);
    assertEquals(1, dialog.leftClickGestures.length);
  });
});