chromium/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_add_dialog_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, FaceGazeAddActionDialogElement} from 'chrome://os-settings/lazy_load.js';
import {CrButtonElement, CrSettingsPrefs, CrSliderElement, FaceGazeSubpageBrowserProxyImpl, IronListElement, Router, routes, SettingsPrefsElement} 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 {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {pressAndReleaseKeyOn} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.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 {isVisible} from 'chrome://webui-test/test_util.js';

import {clearBody} from '../utils.js';

import {TestFaceGazeSubpageBrowserProxy} from './test_facegaze_subpage_browser_proxy.js';

suite('<facegaze-actions-add-dialog>', () => {
  let faceGazeAddActionDialog: FaceGazeAddActionDialogElement;
  let browserProxy: TestFaceGazeSubpageBrowserProxy;
  let prefElement: SettingsPrefsElement;

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

    await CrSettingsPrefs.initialized;
    faceGazeAddActionDialog =
        document.createElement('facegaze-actions-add-dialog');
    faceGazeAddActionDialog.prefs = prefElement.prefs;
    document.body.appendChild(faceGazeAddActionDialog);

    // Assume default open to SELECT_ACTION page.
    assertEquals(
        AddDialogPage.SELECT_ACTION,
        faceGazeAddActionDialog.getCurrentPageForTest());
    flush();
  }

  function getActionsList(): IronListElement|null {
    return faceGazeAddActionDialog.shadowRoot!.querySelector<IronListElement>(
        '#faceGazeAvailableActionsList');
  }

  function assertActionsList(): IronListElement {
    const actionList = getActionsList();
    assertTrue(!!actionList);
    return actionList;
  }

  function assertActionsListNoSelection(): void {
    const actionList = getActionsList();
    assertTrue(!!actionList);
    assertNull(actionList.selectedItem);
  }

  function assertNullActionsList(): void {
    const actionList = getActionsList();
    assertNull(actionList);
  }

  function setActionsListSelection() {
    const actionList = assertActionsList();

    // Cast to Object to satisfy typing for IronListElement.selectedItem.
    actionList.selectedItem = MacroName.MOUSE_CLICK_LEFT as Object;
  }

  function getGesturesList(): IronListElement|null {
    const gestureList: IronListElement|null =
        faceGazeAddActionDialog.shadowRoot!.querySelector<IronListElement>(
            '#faceGazeAvailableGesturesList');
    return gestureList;
  }

  function assertGesturesList(): IronListElement {
    const gestureList = getGesturesList();
    assertTrue(!!gestureList);
    return gestureList;
  }

  function assertGesturesListNoSelection() {
    const gestureList = assertGesturesList();
    assertNull(gestureList.selectedItem);
  }

  function assertNullGesturesList() {
    const gestureList = getGesturesList();
    assertNull(gestureList);
  }

  function setGesturesListSelection() {
    const gestureList = assertGesturesList();

    // Cast to Object to satisfy typing for IronListElement.selectedItem.
    gestureList.selectedItem = FacialGesture.BROW_INNER_UP as Object;
  }

  function getGestureSlider(): CrSliderElement|null {
    const gestureSlider =
        faceGazeAddActionDialog.shadowRoot!.querySelector<CrSliderElement>(
            '#faceGazeGestureThresholdSlider');
    return gestureSlider;
  }

  function assertGestureSlider(): CrSliderElement {
    const gestureSlider = getGestureSlider();
    assertTrue(!!gestureSlider);
    return gestureSlider;
  }

  function assertNullGestureSlider() {
    const gestureSlider = getGestureSlider();
    assertNull(gestureSlider);
  }

  function assertGestureDynamicBar(): HTMLElement {
    const slider = assertGestureSlider();
    const sliderBar = slider.shadowRoot!.querySelector<HTMLElement>('#bar');
    assertTrue(!!sliderBar);
    return sliderBar;
  }

  function getGestureCountDiv(): HTMLElement {
    const gestureCountDiv =
        faceGazeAddActionDialog.shadowRoot!.querySelector<HTMLElement>(
            '#faceGazeGestureCount');
    assertTrue(!!gestureCountDiv);
    return gestureCountDiv;
  }

  function isThresholdValueSetInPref(value: number): boolean {
    const gesturesToConfidence = faceGazeAddActionDialog.prefs.settings.a11y
                                     .face_gaze.gestures_to_confidence.value;
    if (FacialGesture.BROW_INNER_UP in gesturesToConfidence) {
      return gesturesToConfidence[FacialGesture.BROW_INNER_UP] === value;
    }

    return false;
  }

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

  function getCancelButton(): CrButtonElement {
    return getButton('.cancel-button');
  }

  function getActionNextButton(): CrButtonElement {
    return getButton('#faceGazeAddActionNextButton');
  }

  function getGestureNextButton(): CrButtonElement {
    return getButton('#faceGazeGestureNextButton');
  }

  function getGesturePreviousButton(): CrButtonElement {
    const previousButton = getButton('#faceGazeGesturePreviousButton');
    assertFalse(previousButton.disabled);
    return previousButton;
  }

  function getThresholdPreviousButton(): CrButtonElement {
    const previousButton = getButton('#faceGazeThresholdPreviousButton');
    assertFalse(previousButton.disabled);
    return previousButton;
  }

  function getSaveButton(): CrButtonElement {
    const saveButton = getButton('#faceGazeActionDialogSaveButton');
    assertFalse(saveButton.disabled);
    return saveButton;
  }

  function navigateToThresholdPage(): void {
    assertActionsListNoSelection();
    setActionsListSelection();

    const actionNextButton = getActionNextButton();
    assertFalse(actionNextButton.disabled);
    actionNextButton.click();
    flush();

    assertGesturesListNoSelection();
    setGesturesListSelection();

    const gestureNextButton = getGestureNextButton();
    assertFalse(gestureNextButton.disabled);
    gestureNextButton.click();
    flush();

    assertGestureSlider();
    assertNullGesturesList();
  }

  setup(() => {
    browserProxy = new TestFaceGazeSubpageBrowserProxy();
    FaceGazeSubpageBrowserProxyImpl.setInstanceForTesting(browserProxy);
    clearBody();
    Router.getInstance().navigateTo(routes.MANAGE_FACEGAZE_SETTINGS);
  });

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

  test(
      'action page next button disabled if no action is selected', async () => {
        await initPage();
        assertActionsListNoSelection();

        const nextButton = getActionNextButton();
        assertTrue(nextButton.disabled);
      });

  test('action page next button changes dialog to gesture page', async () => {
    await initPage();
    assertActionsListNoSelection();
    setActionsListSelection();

    const nextButton = getActionNextButton();
    assertFalse(nextButton.disabled);

    nextButton.click();
    flush();

    assertGesturesListNoSelection();
    assertNullActionsList();
  });

  test(
      'gesture page previous button changes dialog to action page',
      async () => {
        await initPage();
        assertActionsListNoSelection();
        setActionsListSelection();

        const nextButton = getActionNextButton();
        assertFalse(nextButton.disabled);

        nextButton.click();
        flush();

        assertGesturesListNoSelection();

        const previousButton = getGesturePreviousButton();
        assertFalse(previousButton.disabled);

        previousButton.click();
        flush();

        assertActionsList();
        assertNullGesturesList();
      });

  test(
      'gesture page next button disabled if no gesture is selected',
      async () => {
        await initPage();
        assertActionsListNoSelection();
        setActionsListSelection();

        const actionNextButton = getActionNextButton();
        assertFalse(actionNextButton.disabled);

        actionNextButton.click();
        flush();

        assertGesturesListNoSelection();

        const gestureNextButton = getGestureNextButton();
        assertTrue(gestureNextButton.disabled);
      });

  test(
      'gesture page next button changes dialog to threshold page', async () => {
        await initPage();
        navigateToThresholdPage();
      });

  test(
      'threshold page previous button changes dialog to gesture page',
      async () => {
        await initPage();
        navigateToThresholdPage();

        const previousButton = getThresholdPreviousButton();
        assertFalse(previousButton.disabled);

        previousButton.click();
        flush();

        assertGesturesList();
        assertNullGestureSlider();
      });

  test('threshold page cancel button closes dialog', async () => {
    await initPage();
    navigateToThresholdPage();

    const cancelButton = getCancelButton();
    cancelButton.click();
    flush();

    assertFalse(faceGazeAddActionDialog.$.dialog.open);
  });

  test(
      'threshold page slider changes gesture confidence pref on save',
      async () => {
        await initPage();
        navigateToThresholdPage();

        const gestureSlider = getGestureSlider();
        assertTrue(!!gestureSlider);

        pressAndReleaseKeyOn(gestureSlider, 39 /* right */, [], 'ArrowRight');
        flush();

        const saveButton = getSaveButton();
        saveButton.click();
        flush();

        assertTrue(isThresholdValueSetInPref(65));
        assertFalse(faceGazeAddActionDialog.$.dialog.open);
      });

  test(
      'threshold page slider button changes gesture confidence pref on save',
      async () => {
        await initPage();
        navigateToThresholdPage();

        const gestureSlider = getGestureSlider();
        assertTrue(!!gestureSlider);

        const decrementButton =
            getButton('#faceGazeGestureThresholdDecrementButton');
        decrementButton.click();
        flush();

        const saveButton = getSaveButton();
        saveButton.click();
        flush();

        assertTrue(isThresholdValueSetInPref(55));
        assertFalse(faceGazeAddActionDialog.$.dialog.open);
      });

  test(
      'action page switches to gesture page when initialPage value is set',
      async () => {
        await initPage();

        faceGazeAddActionDialog.actionToAssignGesture =
            MacroName.MOUSE_CLICK_LEFT;
        faceGazeAddActionDialog.initialPage = AddDialogPage.SELECT_GESTURE;
        flush();

        assertGesturesListNoSelection();

        const previousButton =
            faceGazeAddActionDialog.shadowRoot!.querySelector<CrButtonElement>(
                '#faceGazeGesturePreviousButton');
        assertNull(previousButton);
      });

  test(
      'action page switches to threshold page when initialPage value is set',
      async () => {
        await initPage();

        faceGazeAddActionDialog.gestureToConfigure = FacialGesture.BROWS_DOWN;
        faceGazeAddActionDialog.initialPage = AddDialogPage.GESTURE_THRESHOLD;
        flush();

        assertGestureSlider();

        // Previous button should not be shown.
        const previousButton =
            faceGazeAddActionDialog.shadowRoot!.querySelector<CrButtonElement>(
                '#faceGazeThresholdPreviousButton');
        assertNull(previousButton);
      });

  test(
      'browser proxy sends event when page changes to and from threshold page',
      async () => {
        await initPage();
        assertActionsListNoSelection();
        setActionsListSelection();

        const actionNextButton = getActionNextButton();
        assertFalse(actionNextButton.disabled);
        actionNextButton.click();
        flush();
        assertGesturesListNoSelection();
        setGesturesListSelection();
        assertEquals(
            0, browserProxy.getCallCount('toggleGestureInfoForSettings'));

        const gestureNextButton = getGestureNextButton();
        assertFalse(gestureNextButton.disabled);
        gestureNextButton.click();
        flush();
        assertEquals(
            1, browserProxy.getCallCount('toggleGestureInfoForSettings'));
        assertTrue(browserProxy.getArgs('toggleGestureInfoForSettings')[0][0]);
        assertGestureSlider();
        assertNullGesturesList();

        const previousButton = getThresholdPreviousButton();
        assertFalse(previousButton.disabled);
        previousButton.click();
        flush();
        assertEquals(
            2, browserProxy.getCallCount('toggleGestureInfoForSettings'));
        assertFalse(browserProxy.getArgs('toggleGestureInfoForSettings')[1][0]);
      });

  test(
      'gesture detection count updates when gesture info received with selected gesture over threshold',
      async () => {
        await initPage();
        navigateToThresholdPage();

        webUIListenerCallback('settings.sendGestureInfoToSettings', [
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 70},
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 50},
        ]);

        const gestureCountDiv = getGestureCountDiv();

        // Default confidence threshold is 60, so only one gesture should
        // register as detected.
        assertEquals(`Detected 1 time`, gestureCountDiv.innerText);
      });

  test(
      'gesture detection count updates when gesture info received with multiple selected gestures over threshold',
      async () => {
        await initPage();
        navigateToThresholdPage();

        webUIListenerCallback('settings.sendGestureInfoToSettings', [
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 70},
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 80},
        ]);

        const gestureCountDiv = getGestureCountDiv();

        // Default confidence threshold is 60, so two gestures should register
        // as detected.
        assertEquals(`Detected 2 times`, gestureCountDiv.innerText);
      });

  test(
      'gesture detection count does not update when gesture info received with non-selected gesture',
      async () => {
        await initPage();
        navigateToThresholdPage();

        webUIListenerCallback('settings.sendGestureInfoToSettings', [
          {gesture: FacialGesture.JAW_LEFT, confidence: 70},
          {gesture: FacialGesture.EYES_BLINK, confidence: 50},
        ]);

        const gestureCountDiv = getGestureCountDiv();
        assertEquals(`Not detected`, gestureCountDiv.innerText);
      });
  test(
      'gesture threshold dynamic bar updates when gesture info received with selected gesture info at any confidence',
      async () => {
        await initPage();
        navigateToThresholdPage();

        const sliderBar = assertGestureDynamicBar();

        webUIListenerCallback('settings.sendGestureInfoToSettings', [
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 70},
        ]);
        assertEquals('70%', sliderBar.style.width);

        webUIListenerCallback('settings.sendGestureInfoToSettings', [
          {gesture: FacialGesture.BROW_INNER_UP, confidence: 30},
        ]);
        assertEquals('30%', sliderBar.style.width);
      });
});