chromium/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/facegaze_test.js

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

GEN_INCLUDE(['facegaze_test_base.js']);

FaceGazeTest = class extends FaceGazeTestBase {
  /** @override */
  testGenPreamble() {
    super.testGenPreamble();
    super.testGenPreambleCommon(
        /*extensionIdName=*/ 'kAccessibilityCommonExtensionId',
        /*failOnConsoleError=*/ true);
  }

  assertMouseClickAt(args) {
    const {pressEvent, releaseEvent, isLeft, x, y} = args;
    const button = isLeft ?
        this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT :
        this.mockAccessibilityPrivate.SyntheticMouseEventButton.RIGHT;
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventType.PRESS,
        pressEvent.type);
    assertEquals(button, pressEvent.mouseButton);
    assertEquals(x, pressEvent.x);
    assertEquals(y, pressEvent.y);
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventType.RELEASE,
        releaseEvent.type);
    assertEquals(button, releaseEvent.mouseButton);
    assertEquals(x, releaseEvent.x);
    assertEquals(y, releaseEvent.y);
  }
};

AX_TEST_F(
    'FaceGazeTest', 'FacialGesturesInFacialGesturesToMediapipeGestures', () => {
      // Tests that all new FacialGestures are mapped to
      // MediapipeFacialGestures. FacialGestures are those set by the user,
      // while MediapipeFacialGestures are the raw gestures recognized by
      // Mediapipe. This is to help developers remember to add gestures to both.
      const facialGestures = Object.values(FacialGesture);
      const facialGesturesToMediapipeGestures =
          new Set(FacialGesturesToMediapipeGestures.keys().toArray());
      assertEquals(
          facialGestures.length, facialGesturesToMediapipeGestures.size);

      for (const gesture of facialGestures) {
        assertTrue(facialGesturesToMediapipeGestures.has(gesture));
      }
    });

AX_TEST_F(
    'FaceGazeTest',
    'GestureDetectorUpdatesStateAfterToggleGestureInfoForSettingsEvent',
    async function() {
      await this.configureFaceGaze(new Config());

      // Tests that GestureDetector updates its state after a
      // toggleGestureInfoForSettings event is received from
      // chrome.accessibilityPrivate.
      this.mockAccessibilityPrivate.toggleGestureInfoForSettings(false);
      assertFalse(GestureDetector.shouldSendGestureDetectionInfo_);

      this.mockAccessibilityPrivate.toggleGestureInfoForSettings(true);
      assertTrue(GestureDetector.shouldSendGestureDetectionInfo_);
    });

AX_TEST_F(
    'FaceGazeTest',
    'GestureDetectorSendsGestureInfoAfterToggleGestureInfoForSettingsEvent',
    async function() {
      const gestureToMacroName =
          new Map()
              .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_RIGHT)
              .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_CLICK_LEFT);
      const gestureToConfidence = new Map()
                                      .set(FacialGesture.BROW_INNER_UP, 0.6)
                                      .set(FacialGesture.JAW_OPEN, 0.6);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence);
      await this.configureFaceGaze(config);

      // Toggle sending on.
      this.mockAccessibilityPrivate.toggleGestureInfoForSettings(true);
      assertTrue(GestureDetector.shouldSendGestureDetectionInfo_);

      const result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3)
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9);
      this.processFaceLandmarkerResult(result);

      // Assert both values are sent.
      assertEquals(
          this.mockAccessibilityPrivate.getSendGestureInfoToSettingsCount(), 1);
      const gestureInfo =
          this.mockAccessibilityPrivate.getFaceGazeGestureInfo();
      assertEquals(gestureInfo.length, 2);
      assertEquals(gestureInfo[0].gesture, FacialGesture.BROW_INNER_UP);
      assertEquals(gestureInfo[0].confidence, 30);
      assertEquals(gestureInfo[1].gesture, FacialGesture.JAW_OPEN);
      assertEquals(gestureInfo[1].confidence, 90);
    });

AX_TEST_F(
    'FaceGazeTest',
    'GestureDetectorDoesNotSendGestureInfoIfNoToggleGestureInfoForSettingsEvent',
    async function() {
      const gestureToMacroName =
          new Map()
              .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_RIGHT)
              .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_CLICK_LEFT);
      const gestureToConfidence = new Map()
                                      .set(FacialGesture.BROW_INNER_UP, 0.6)
                                      .set(FacialGesture.JAW_OPEN, 0.6);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence);
      await this.configureFaceGaze(config);

      const result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3)
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9);
      this.processFaceLandmarkerResult(result);

      // Assert no call is made.
      assertEquals(
          0, this.mockAccessibilityPrivate.getSendGestureInfoToSettingsCount());
    });

AX_TEST_F('FaceGazeTest', 'IntervalReusesForeheadLocation', async function() {
  const config =
      new Config().withMouseLocation({x: 600, y: 400}).withBufferSize(1);
  await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

  // Manually executing the mouse interval sets the cursor position with the
  // most recent forehead location.
  for (let i = 0; i < 3; i++) {
    this.mockAccessibilityPrivate.clearCursorPosition();
    this.triggerMouseControllerInterval();
    this.assertLatestCursorPosition({x: 600, y: 400});
  }
});

AX_TEST_F('FaceGazeTest', 'CursorPositionUpdatedOnInterval', async function() {
  const config =
      new Config().withMouseLocation({x: 600, y: 400}).withBufferSize(1);
  await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

  const result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.2, 0.4);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // Cursor position doesn't change on result.
  this.assertLatestCursorPosition({x: 600, y: 400});

  // Cursor position does change after interval fired.
  this.triggerMouseControllerInterval();
  const cursorPosition =
      this.mockAccessibilityPrivate.getLatestCursorPosition();
  assertNotEquals(600, cursorPosition.x);
  assertNotEquals(400, cursorPosition.y);
});

AX_TEST_F('FaceGazeTest', 'UpdateMouseLocation', async function() {
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withBufferSize(1)
                     .withCursorControlEnabled(true);
  await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

  // Move left and down. Note that increasing the x coordinate results in
  // moving left because the image is mirrored, while increasing y moves
  // downward as expected.
  let result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.11, 0.21);
  this.processFaceLandmarkerResult(result);

  this.assertLatestCursorPosition({x: 360, y: 560});

  // Move to where we were. Since the buffer size is 1, should end up at the
  // exact same location.
  result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.1, 0.2);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 600, y: 400});

  // Try a larger movement, 10% of the screen.
  result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.12, 0.22);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 120, y: 720});

  result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.1, 0.2);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 600, y: 400});

  // Try a very small movement, 0.5% of the screen, which ends up being about
  // one pixel.
  result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
      0.101, 0.201);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 580, y: 420});
});

AX_TEST_F(
    'FaceGazeTest', 'UpdatesMousePositionOnlyWhenCursorControlEnabled',
    async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withCursorControlEnabled(false);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

      // Move left and down. No events are generated.
      let result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.11, 0.21);
      this.processFaceLandmarkerResult(result);

      assertEquals(
          null, this.mockAccessibilityPrivate.getLatestCursorPosition());

      // Try moving back. Still nothing.
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.1, 0.2);
      this.processFaceLandmarkerResult(result);
      assertEquals(
          null, this.mockAccessibilityPrivate.getLatestCursorPosition());

      // Turn on cursor control.
      await this.setPref(FaceGaze.PREF_CURSOR_CONTROL_ENABLED, true);

      // Now head movement should do something.
      // This is the first detected head movement and should end up at the
      // original mouse location.
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.11, 0.21);
      this.processFaceLandmarkerResult(result);
      this.assertLatestCursorPosition({x: 600, y: 400});

      // Moving the head further should move the mouse away from the original
      // cursor position.
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.12, 0.22);
      this.processFaceLandmarkerResult(result);

      this.assertLatestCursorPosition({x: 360, y: 560});

      // Turn it off again and move the mouse further. Nothing should happen.
      await this.setPref(FaceGaze.PREF_CURSOR_CONTROL_ENABLED, false);
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.13, 0.23);
      this.processFaceLandmarkerResult(result);
      this.assertLatestCursorPosition({x: 360, y: 560});
    });

// Test that if the forehead location is moving around a different part of
// the screen, it still has the same offsets (i.e. we aren't tracking
// absolute forehead position, but instead relative).
// This test should use the same cursor positions as the previous version,
// but different forehead locations (with the same offsets).
AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationFromDifferentForeheadLocation',
    async function() {
      const config =
          new Config().withMouseLocation({x: 600, y: 400}).withBufferSize(1);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.6, 0.7);

      // Move left and down. Note that increasing the x coordinate results in
      // moving left because the image is mirrored.
      let result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.61, 0.71);
      this.processFaceLandmarkerResult(result);
      this.assertLatestCursorPosition({x: 360, y: 560});

      // Move to where we were. Since the buffer size is 1, should end up at the
      // exact same location.
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.6, 0.7);
      this.processFaceLandmarkerResult(result);
      this.assertLatestCursorPosition({x: 600, y: 400});
    });

// Tests that left/top offsets in ScreenBounds are respected. This should have
// the same results as the first test offset by exactly left/top.
AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithScreenNotAtZero', async function() {
      this.mockAccessibilityPrivate.setDisplayBounds(
          [{left: 100, top: 50, width: 1200, height: 800}]);

      const config =
          new Config().withMouseLocation({x: 700, y: 450}).withBufferSize(1);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

      // Move left and down. Note that increasing the x coordinate results in
      // moving left because the image is mirrored, while increasing y moves
      // downward as expected.
      let result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.11, 0.21);
      this.processFaceLandmarkerResult(result);

      this.assertLatestCursorPosition({x: 460, y: 610});

      // Move to where we were. Since the buffer size is 1, should end up at the
      // exact same location.
      result = new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
          0.1, 0.2);
      this.processFaceLandmarkerResult(result);
      this.assertLatestCursorPosition({x: 700, y: 450});
    });

AX_TEST_F('FaceGazeTest', 'UpdateMouseLocationWithBuffer', async function() {
  const config =
      new Config().withMouseLocation({x: 600, y: 400}).withBufferSize(6);
  await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

  // Move left and down. Note that increasing the x coordinate results in
  // moving left because the image is mirrored.
  let result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.11, 0.21);
  this.processFaceLandmarkerResult(result);
  let cursorPosition = this.mockAccessibilityPrivate.getLatestCursorPosition();
  assertTrue(cursorPosition.x < 600);
  assertTrue(cursorPosition.y > 400);

  // Move right and up. Due to smoothing, we don't exactly reach (600,400)
  // again, but do get closer to it.
  result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.1, 0.2);
  this.processFaceLandmarkerResult(result);
  let newCursorPosition =
      this.mockAccessibilityPrivate.getLatestCursorPosition();
  assertTrue(newCursorPosition.x > cursorPosition.x);
  assertTrue(newCursorPosition.y < cursorPosition.y);
  assertTrue(newCursorPosition.x < 600);
  assertTrue(newCursorPosition.y > 400);

  cursorPosition = newCursorPosition;
  // Process the same result again. We move even closer to (600, 400).
  this.processFaceLandmarkerResult(result);
  newCursorPosition = this.mockAccessibilityPrivate.getLatestCursorPosition();
  assertTrue(newCursorPosition.x > cursorPosition.x);
  assertTrue(newCursorPosition.y < cursorPosition.y);
  assertTrue(newCursorPosition.x < 600);
  assertTrue(newCursorPosition.y > 400);
});

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithSpeed1Move1', async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

      // With mouse acceleration off and buffer size 1, one-pixel head movements
      // correspond to one-pixel changes on screen.
      const px = 1.0 / 1200;
      const py = 1.0 / 800;

      for (let i = 1; i < 10; i++) {
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                0.1 + px * i, 0.2 + py * i);
        this.processFaceLandmarkerResult(result);
        this.assertLatestCursorPosition({x: 600 - i, y: 400 + i});
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithSpeed1Move5', async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);
      const px = 1.0 / 1200;
      const py = 1.0 / 800;

      // Move further. Should get a linear increase in position.
      for (let i = 0; i < 5; i++) {
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                0.1 + px * i * 5, 0.2 + py * i * 5);
        this.processFaceLandmarkerResult(result);
        this.assertLatestCursorPosition({x: 600 - i * 5, y: 400 + i * 5});
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithSpeed1Move20', async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1);
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);
      const px = 1.0 / 1200;
      const py = 1.0 / 800;

      // Move even further. Should get a linear increase in position.
      for (let i = 0; i < 5; i++) {
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                0.1 + px * i * 20, 0.2 + py * i * 20);
        this.processFaceLandmarkerResult(result);
        this.assertLatestCursorPosition({x: 600 - i * 20, y: 400 + i * 20});
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithAccelerationMove1',
    async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1)
                         .withMouseAcceleration();
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

      // With mouse acceleration off and buffer size 1, one-pixel head movements
      // correspond to one-pixel changes on screen.
      const px = 1.0 / 1200;
      const py = 1.0 / 800;
      let xLocation = 0.1;
      let yLocation = 0.2;

      // With these settings, moving the face by one pixel at a time is not ever
      // enough to move the cursor.
      for (let i = 1; i < 10; i++) {
        xLocation += px;
        yLocation += py;
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                xLocation, yLocation);
        this.processFaceLandmarkerResult(result);
        this.assertLatestCursorPosition({x: 600, y: 400});
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithAccelerationMove5',
    async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1)
                         .withMouseAcceleration();
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);
      const px = 1.0 / 1200;
      const py = 1.0 / 800;
      let xLocation = 0.1;
      let yLocation = 0.2;

      // Move by 5 pixels at a time to get a non-linear increase of 3 (movement
      // still dampened by sigmoid).
      for (let i = 1; i < 5; i++) {
        xLocation += px * 5;
        yLocation += py * 5;
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                xLocation, yLocation);
        this.processFaceLandmarkerResult(result);
        this.assertLatestCursorPosition({x: 600 - i * 3, y: 400 + i * 3});
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithAccelerationMove10',
    async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1)
                         .withMouseAcceleration();
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

      // With mouse acceleration off and buffer size 1, one-pixel head movements
      // correspond to one-pixel changes on screen.
      const px = 1.0 / 1200;
      const py = 1.0 / 800;
      let xLocation = 0.1;
      let yLocation = 0.2;

      // Move by 10 pixels at a time to get a linear increase of 10 (sigma at
      // 1).
      for (let i = 1; i < 5; i++) {
        xLocation += px * 10;
        yLocation += py * 10;
        const initialCursorPosition =
            this.mockAccessibilityPrivate.getLatestCursorPosition();
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                xLocation, yLocation);
        this.processFaceLandmarkerResult(result);
        const cursorPosition =
            this.mockAccessibilityPrivate.getLatestCursorPosition();
        assertEquals(-10, cursorPosition.x - initialCursorPosition.x);
        assertEquals(10, cursorPosition.y - initialCursorPosition.y);
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'UpdateMouseLocationWithAccelerationMove20',
    async function() {
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withBufferSize(1)
                         .withSpeeds(1, 1, 1, 1)
                         .withMouseAcceleration();
      await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);
      const px = 1.0 / 1200;
      const py = 1.0 / 800;
      let xLocation = 0.1;
      let yLocation = 0.2;

      // Finally, do a large movement of 20 px. It should be magnified further.
      for (let i = 1; i < 5; i++) {
        xLocation += px * 20;
        yLocation += py * 20;
        const initialCursorPosition =
            this.mockAccessibilityPrivate.getLatestCursorPosition();
        const result =
            new MockFaceLandmarkerResult().setNormalizedForeheadLocation(
                xLocation, yLocation);
        this.processFaceLandmarkerResult(result);
        const cursorPosition =
            this.mockAccessibilityPrivate.getLatestCursorPosition();
        assertEquals(-24, cursorPosition.x - initialCursorPosition.x);
        assertEquals(24, cursorPosition.y - initialCursorPosition.y);
      }
    });

AX_TEST_F(
    'FaceGazeTest', 'DetectGesturesAndPerformShortActions', async function() {
      const gestureToMacroName =
          new Map()
              .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_CLICK_LEFT)
              .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_RIGHT);
      const gestureToConfidence = new Map()
                                      .set(FacialGesture.JAW_OPEN, 0.6)
                                      .set(FacialGesture.BROW_INNER_UP, 0.6);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence);
      await this.configureFaceGaze(config);

      let result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(2);
      const pressEvent = this.getMouseEvents()[0];
      const releaseEvent = this.getMouseEvents()[1];
      this.assertMouseClickAt(
          {pressEvent, releaseEvent, isLeft: true, x: 600, y: 400});

      // Release all gestures, nothing should happen.
      result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.4)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      // No more events are generated.
      this.assertNumMouseEvents(2);
    });


AX_TEST_F(
    'FaceGazeTest', 'DetectGesturesAndPerformLongActions', async function() {
      const gestureToMacroName =
          new Map()
              .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_LONG_CLICK_LEFT)
              .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_RIGHT);
      const gestureToConfidence = new Map()
                                      .set(FacialGesture.JAW_OPEN, 0.6)
                                      .set(FacialGesture.BROW_INNER_UP, 0.6);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence);
      await this.configureFaceGaze(config);

      let result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(1);
      const pressEvent = this.getMouseEvents()[0];
      assertEquals(
          this.mockAccessibilityPrivate.SyntheticMouseEventType.PRESS,
          pressEvent.type);
      assertEquals(
          this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
          pressEvent.mouseButton);
      assertEquals(600, pressEvent.x);
      assertEquals(400, pressEvent.y);

      // Reduce amount of jaw open to get the release event.
      result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.4)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(2);
      const releaseEvent = this.getMouseEvents()[1];
      assertEquals(
          this.mockAccessibilityPrivate.SyntheticMouseEventType.RELEASE,
          releaseEvent.type);
      assertEquals(
          this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
          releaseEvent.mouseButton);
      assertEquals(600, releaseEvent.x);
      assertEquals(400, releaseEvent.y);
    });

// The BrowDown gesture is special because it is the combination of two
// separate facial gestures. This test ensures that the associated action is
// performed if either of the gestures is detected.
AX_TEST_F('FaceGazeTest', 'BrowDownGesture', async function() {
  const gestureToMacroName =
      new Map().set(FacialGesture.BROWS_DOWN, MacroName.RESET_CURSOR);
  const gestureToConfidence = new Map().set(FacialGesture.BROWS_DOWN, 0.6);
  const config = new Config()
                     .withMouseLocation({x: 0, y: 0})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence);
  await this.configureFaceGaze(config);
  this.mockAccessibilityPrivate.clearCursorPosition();

  let result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_DOWN_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  assertEquals(null, this.mockAccessibilityPrivate.getLatestCursorPosition());

  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_DOWN_LEFT, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 600, y: 400});
  this.mockAccessibilityPrivate.clearCursorPosition();
  this.clearGestureLastRecognizedTime();

  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_DOWN_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.9);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 600, y: 400});
  this.mockAccessibilityPrivate.clearCursorPosition();
  this.clearGestureLastRecognizedTime();

  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_DOWN_LEFT, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.9);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 600, y: 400});
});

AX_TEST_F(
    'FaceGazeTest', 'DoesNotPerformActionsWhenActionsDisabled',
    async function() {
      const gestureToMacroName =
          new Map()
              .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_CLICK_LEFT)
              .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_RIGHT);
      const gestureToConfidence = new Map()
                                      .set(FacialGesture.JAW_OPEN, 0.6)
                                      .set(FacialGesture.BROW_INNER_UP, 0.6);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence)
                         .withCursorControlEnabled(true)
                         .withActionsEnabled(false);
      await this.configureFaceGaze(config);

      let result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(0);

      result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.9);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(0);

      // Enable actions. Now we we should get actions.
      await this.setPref(FaceGaze.PREF_ACTIONS_ENABLED, true);

      result =
          new MockFaceLandmarkerResult()
              .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
              .addGestureWithConfidence(
                  MediapipeFacialGesture.BROW_INNER_UP, 0.3);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(2);
      const pressEvent = this.getMouseEvents()[0];
      const releaseEvent = this.getMouseEvents()[1];
      this.assertMouseClickAt(
          {pressEvent, releaseEvent, isLeft: true, x: 600, y: 400});
    });

AX_TEST_F(
    'FaceGazeTest', 'ActionsUseMouseLocationWhenCursorControlDisabled',
    async function() {
      const gestureToMacroName = new Map().set(
          FacialGesture.MOUTH_PUCKER, MacroName.MOUSE_CLICK_RIGHT);
      const gestureToConfidence =
          new Map().set(FacialGesture.MOUTH_PUCKER, 0.5);
      const config = new Config()
                         .withMouseLocation({x: 600, y: 400})
                         .withGestureToMacroName(gestureToMacroName)
                         .withGestureToConfidence(gestureToConfidence)
                         .withActionsEnabled(true)
                         .withCursorControlEnabled(false);
      await this.configureFaceGaze(config);

      // Even though the mouse controller is off, automation mouse events still
      // change the mouse position for click actions.
      this.sendAutomationMouseEvent(
          {mouseX: 350, mouseY: 250, eventFrom: 'user'});

      result = new MockFaceLandmarkerResult().addGestureWithConfidence(
          MediapipeFacialGesture.MOUTH_PUCKER, 0.7);
      this.processFaceLandmarkerResult(result);

      this.assertNumMouseEvents(2);
      const pressEvent = this.getMouseEvents()[0];
      const releaseEvent = this.getMouseEvents()[1];
      this.assertMouseClickAt(
          {pressEvent, releaseEvent, isLeft: false, x: 350, y: 250});
    });

AX_TEST_F('FaceGazeTest', 'DoesNotRepeatGesturesTooSoon', async function() {
  const gestureToMacroName =
      new Map()
          .set(FacialGesture.JAW_OPEN, MacroName.MOUSE_LONG_CLICK_LEFT)
          .set(FacialGesture.BROW_INNER_UP, MacroName.RESET_CURSOR)
          .set(FacialGesture.BROWS_DOWN, MacroName.MOUSE_CLICK_RIGHT);
  const gestureToConfidence = new Map()
                                  .set(FacialGesture.JAW_OPEN, 0.6)
                                  .set(FacialGesture.BROW_INNER_UP, 0.6)
                                  .set(FacialGesture.BROWS_DOWN, 0.6);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence)
                     .withRepeatDelayMs(1000);
  await this.configureFaceGaze(config);

  for (let i = 0; i < 5; i++) {
    const result =
        new MockFaceLandmarkerResult()
            .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
            .addGestureWithConfidence(
                MediapipeFacialGesture.BROW_INNER_UP, 0.3);
    this.processFaceLandmarkerResult(result);

    // 5 times in quick succession still only generates one press.
    this.assertNumMouseEvents(1);
    const pressEvent = this.getMouseEvents()[0];
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventType.PRESS,
        pressEvent.type);
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
        pressEvent.mouseButton);
  }

  // Release is generated when the JAW_OPEN ends.
  let result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.5)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.3);
  this.processFaceLandmarkerResult(result);
  this.assertNumMouseEvents(2);
  let releaseEvent = this.getMouseEvents()[1];
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventType.RELEASE,
      releaseEvent.type);
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
      releaseEvent.mouseButton);

  // Another gesture is let through once and then also throttled.
  for (let i = 0; i < 5; i++) {
    const result = new MockFaceLandmarkerResult()
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.BROW_DOWN_LEFT, 0.9)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.9);
    this.processFaceLandmarkerResult(result);

    this.assertNumMouseEvents(4);
    const pressEvent = this.getMouseEvents()[2];
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventType.PRESS,
        pressEvent.type);
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventButton.RIGHT,
        pressEvent.mouseButton);
    releaseEvent = this.getMouseEvents()[3];
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventType.RELEASE,
        releaseEvent.type);
    assertEquals(
        this.mockAccessibilityPrivate.SyntheticMouseEventButton.RIGHT,
        releaseEvent.mouseButton);
  }

  // Release is processed when BROWS_DOWN ends.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_DOWN_LEFT, 0.4)
          .addGestureWithConfidence(
              MediapipeFacialGesture.BROW_DOWN_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  this.assertNumMouseEvents(4);
});

AX_TEST_F('FaceGazeTest', 'DoesNotClickDuringLongClick', async function() {
  const gestureToMacroName =
      new Map()
          .set(FacialGesture.MOUTH_PUCKER, MacroName.MOUSE_LONG_CLICK_LEFT)
          .set(FacialGesture.EYE_SQUINT_LEFT, MacroName.MOUSE_CLICK_LEFT)
          .set(FacialGesture.EYE_SQUINT_RIGHT, MacroName.MOUSE_CLICK_RIGHT);

  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName);
  await this.configureFaceGaze(config);

  // Start the long click.
  let result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);

  this.assertNumMouseEvents(1);
  const pressEvent = this.getMouseEvents()[0];
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventType.PRESS,
      pressEvent.type);
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
      pressEvent.mouseButton);
  assertEquals(600, pressEvent.x);
  assertEquals(400, pressEvent.y);

  // Send a short left click gesture while holding long click left by
  // keeping the MOUTH_PUCKER gesture.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  // No more events are generated.
  this.assertNumMouseEvents(1);
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  // No more events are generated.
  this.assertNumMouseEvents(1);

  // Try with short right click gesture while still holding long click left.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.9);
  this.processFaceLandmarkerResult(result);
  // No more events are generated.
  this.assertNumMouseEvents(1);

  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  // No more events are generated.
  this.assertNumMouseEvents(1);

  // Send the end of the long click by stopping mouth pucker.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.MOUTH_PUCKER, 0.4)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.3)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_RIGHT, 0.3);
  this.processFaceLandmarkerResult(result);
  this.assertNumMouseEvents(2);
  const releaseEvent = this.getMouseEvents()[1];
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventType.RELEASE,
      releaseEvent.type);
  assertEquals(
      this.mockAccessibilityPrivate.SyntheticMouseEventButton.LEFT,
      releaseEvent.mouseButton);
  assertEquals(600, releaseEvent.x);
  assertEquals(400, releaseEvent.y);
});

AX_TEST_F('FaceGazeTest', 'KeyEvents', async function() {
  const gestureToMacroName =
      new Map()
          .set(FacialGesture.EYE_SQUINT_LEFT, MacroName.KEY_PRESS_SPACE)
          .set(FacialGesture.EYE_SQUINT_RIGHT, MacroName.KEY_PRESS_UP)
          .set(FacialGesture.MOUTH_SMILE, MacroName.KEY_PRESS_DOWN)
          .set(FacialGesture.MOUTH_UPPER_UP, MacroName.KEY_PRESS_LEFT)
          .set(FacialGesture.EYES_BLINK, MacroName.KEY_PRESS_RIGHT)
          .set(FacialGesture.JAW_OPEN, MacroName.KEY_PRESS_TOGGLE_OVERVIEW)
          .set(
              FacialGesture.MOUTH_PUCKER, MacroName.KEY_PRESS_MEDIA_PLAY_PAUSE);
  const gestureToConfidence = new Map()
                                  .set(FacialGesture.EYE_SQUINT_LEFT, 0.7)
                                  .set(FacialGesture.EYE_SQUINT_RIGHT, 0.7)
                                  .set(FacialGesture.MOUTH_SMILE, 0.7)
                                  .set(FacialGesture.MOUTH_UPPER_UP, 0.7)
                                  .set(FacialGesture.EYES_BLINK, 0.7)
                                  .set(FacialGesture.JAW_OPEN, 0.7)
                                  .set(FacialGesture.MOUTH_PUCKER, 0.7);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence)
                     .withRepeatDelayMs(1000);
  await this.configureFaceGaze(config);

  const makeResultAndProcess = (gestures) => {
    const result = new MockFaceLandmarkerResult()
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.EYE_SQUINT_LEFT,
                           gestures.squintLeft ? gestures.squintLeft : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.EYE_SQUINT_RIGHT,
                           gestures.squintRight ? gestures.squintRight : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.MOUTH_SMILE_LEFT,
                           gestures.smileLeft ? gestures.smileLeft : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.MOUTH_SMILE_RIGHT,
                           gestures.smileRight ? gestures.smileRight : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.MOUTH_UPPER_UP_LEFT,
                           gestures.upperUpLeft ? gestures.upperUpLeft : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.MOUTH_UPPER_UP_RIGHT,
                           gestures.upperUpRight ? gestures.upperUpRight : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.EYE_BLINK_LEFT,
                           gestures.blinkLeft ? gestures.blinkLeft : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.EYE_BLINK_RIGHT,
                           gestures.blinkRight ? gestures.blinkRight : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.JAW_OPEN,
                           gestures.jawOpen ? gestures.jawOpen : 0.3)
                       .addGestureWithConfidence(
                           MediapipeFacialGesture.MOUTH_PUCKER,
                           gestures.mouthPucker ? gestures.mouthPucker : 0.3);
    this.processFaceLandmarkerResult(result);
    return this.getKeyEvents();
  };

  // Squint left for space key press.
  let keyEvents = makeResultAndProcess({squintLeft: .75});
  assertEquals(1, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[0].type);
  assertEquals(KeyCode.SPACE, keyEvents[0].keyCode);

  // Stop squinting left for space key release.
  keyEvents = makeResultAndProcess({});
  assertEquals(2, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[1].type);
  assertEquals(KeyCode.SPACE, keyEvents[1].keyCode);

  // Squint right eye for up key press.
  keyEvents = makeResultAndProcess({squintRight: 0.8});
  assertEquals(3, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[2].type);
  assertEquals(KeyCode.UP, keyEvents[2].keyCode);

  // Start smiling on both sides to create down arrow key press.
  keyEvents = makeResultAndProcess(
      {squintRight: 0.8, smileLeft: 0.9, smileRight: 0.85});
  assertEquals(4, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[3].type);
  assertEquals(KeyCode.DOWN, keyEvents[3].keyCode);

  // Stop squinting right eye for up arrow key release.
  keyEvents = makeResultAndProcess({smileLeft: 0.9, smileRight: 0.85});
  assertEquals(5, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[4].type);
  assertEquals(KeyCode.UP, keyEvents[4].keyCode);

  // Stop smiling for down arrow key release.
  keyEvents = makeResultAndProcess({});
  assertEquals(6, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[5].type);
  assertEquals(KeyCode.DOWN, keyEvents[5].keyCode);

  // Mouth upper up on both sides for left key press.
  keyEvents = makeResultAndProcess({upperUpLeft: 0.9, upperUpRight: 0.8});
  assertEquals(7, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[6].type);
  assertEquals(KeyCode.LEFT, keyEvents[6].keyCode);

  // Blink both eyes for right key press.
  keyEvents = makeResultAndProcess({
    upperUpLeft: 0.85,
    upperUpRight: 0.9,
    blinkLeft: 0.85,
    blinkRight: 0.75,
  });
  assertEquals(8, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[7].type);
  assertEquals(KeyCode.RIGHT, keyEvents[7].keyCode);

  // Stop blinking, get right key up.
  keyEvents = makeResultAndProcess({upperUpLeft: 0.85, upperUpRight: 0.95});
  assertEquals(9, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[8].type);
  assertEquals(KeyCode.RIGHT, keyEvents[8].keyCode);

  // Stop all gestures, get final left key up.
  keyEvents = makeResultAndProcess({});
  assertEquals(10, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[9].type);
  assertEquals(KeyCode.LEFT, keyEvents[9].keyCode);

  // Jaw open for toggle overview key press.
  keyEvents = makeResultAndProcess({jawOpen: .75});
  assertEquals(11, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[10].type);
  assertEquals(KeyCode.MEDIA_LAUNCH_APP1, keyEvents[10].keyCode);

  // Jaw close for toggle overview key release.
  keyEvents = makeResultAndProcess({});
  assertEquals(12, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[11].type);
  assertEquals(KeyCode.MEDIA_LAUNCH_APP1, keyEvents[11].keyCode);

  // Mouth pucker for media play/pause key press.
  keyEvents = makeResultAndProcess({mouthPucker: .75});
  assertEquals(13, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
      keyEvents[12].type);
  assertEquals(KeyCode.MEDIA_PLAY_PAUSE, keyEvents[12].keyCode);

  // Stop mouth pucker for media play/pause key release.
  keyEvents = makeResultAndProcess({});
  assertEquals(14, keyEvents.length);
  assertEquals(
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP,
      keyEvents[13].type);
  assertEquals(KeyCode.MEDIA_PLAY_PAUSE, keyEvents[13].keyCode);
});

// TODO(b/345059065): Test is flaky.
AX_TEST_F('FaceGazeTest', 'DISABLED_ClosesCameraStream', async function() {
  await this.getFaceGaze().cameraStreamReadyPromise_;
  let win = chrome.extension.getViews().find(
      view => view.location.href.includes('camera_stream.html'));
  assertTrue(!!win);
  this.getFaceGaze().onFaceGazeDisabled();
  await this.getFaceGaze().cameraStreamClosedPromise_;
  win = chrome.extension.getViews().find(
      view => view.location.href.includes('camera_stream.html'));
  assertFalse(!!win);
});

// TODO(crbug.com/348603598): Test is flaky.
AX_TEST_F('FaceGazeTest', 'DISABLED_ToggleFaceGazeGesturesShort', async function() {
  const gestureToMacroName =
      new Map()
          .set(FacialGesture.JAW_OPEN, MacroName.TOGGLE_FACEGAZE)
          .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_CLICK_LEFT);
  const gestureToConfidence = new Map()
                                  .set(FacialGesture.JAW_OPEN, 0.3)
                                  .set(FacialGesture.BROW_INNER_UP, 0.3);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence)
                     .withRepeatDelayMs(1);
  await this.configureFaceGaze(config);

  // Toggle (pause) FaceGaze.
  result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  assertTrue(this.getFaceGaze().gestureHandler_.paused_);

  // Try to perform left click.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // No click should be performed.
  this.assertNumMouseEvents(0);

  // Toggle (resume) FaceGaze and release mouse click gesture.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  assertFalse(this.getFaceGaze().gestureHandler_.paused_);
  // No click should be performed.
  this.assertNumMouseEvents(0);

  // Perform left click now that FaceGaze has resumed.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // Synthetic mouse events should have been sent.
  this.assertNumMouseEvents(2);
});

AX_TEST_F('FaceGazeTest', 'ToggleFaceGazeGesturesLong', async function() {
  const gestureToMacroName =
      new Map()
          .set(FacialGesture.JAW_OPEN, MacroName.TOGGLE_FACEGAZE)
          .set(FacialGesture.BROW_INNER_UP, MacroName.MOUSE_LONG_CLICK_LEFT)
          .set(FacialGesture.EYE_SQUINT_LEFT, MacroName.KEY_PRESS_SPACE);
  const gestureToConfidence = new Map()
                                  .set(FacialGesture.JAW_OPEN, 0.3)
                                  .set(FacialGesture.BROW_INNER_UP, 0.3)
                                  .set(FacialGesture.EYE_SQUINT_LEFT, 0.3);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence)
                     .withRepeatDelayMs(1);
  await this.configureFaceGaze(config);

  // Trigger a mouse press and a key down.
  let result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // A synthetic mouse event should have been sent.
  this.assertNumMouseEvents(1);
  this.assertMousePress(this.getMouseEvents()[0]);

  // A synthetic key event should have been sent.
  this.assertNumKeyEvents(1);
  this.assertKeyDown(this.getKeyEvents()[0]);

  // Toggle (pause) FaceGaze in the middle of long actions.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0.9)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  assertTrue(this.getFaceGaze().gestureHandler_.paused_);

  // Pausing in the middle of long actions should cause them to be completed.
  // The purpose of this is to clear state.
  this.assertNumMouseEvents(2);
  this.assertMouseRelease(this.getMouseEvents()[1]);
  this.assertNumKeyEvents(2);
  this.assertKeyUp(this.getKeyEvents()[1]);

  // Release all gestures.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  // No extra mouse or key events should have come through.
  this.assertNumMouseEvents(2);
  this.assertNumKeyEvents(2);

  // Toggle (resume) FaceGaze.
  result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  assertFalse(this.getFaceGaze().gestureHandler_.paused_);
  // No extra mouse or key events should come through.
  this.assertNumMouseEvents(2);
  this.assertNumKeyEvents(2);

  // Confirm that long actions work as expected.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0.9)
          .addGestureWithConfidence(
              MediapipeFacialGesture.EYE_SQUINT_LEFT, 0.9);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // A mouse press should have been sent.
  this.assertNumMouseEvents(3);
  this.assertMousePress(this.getMouseEvents()[2]);

  // A key down should have been sent.
  this.assertNumKeyEvents(3);
  this.assertKeyDown(this.getKeyEvents()[2]);

  // Release gestures to get the mouse release and the key up events.
  result =
      new MockFaceLandmarkerResult()
          .addGestureWithConfidence(MediapipeFacialGesture.BROW_INNER_UP, 0)
          .addGestureWithConfidence(MediapipeFacialGesture.EYE_SQUINT_LEFT, 0);
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);

  // Confirm that the mouse release was sent.
  this.assertNumMouseEvents(4);
  this.assertMouseRelease(this.getMouseEvents()[3]);

  // Confirm that the key up event was sent.
  this.assertNumKeyEvents(4);
  this.assertKeyUp(this.getKeyEvents()[3]);
});

AX_TEST_F('FaceGazeTest', 'ToggleFaceGazeMouseMovement', async function() {
  const gestureToMacroName =
      new Map().set(FacialGesture.JAW_OPEN, MacroName.TOGGLE_FACEGAZE);
  const gestureToConfidence = new Map().set(FacialGesture.JAW_OPEN, 0.3);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withBufferSize(1)
                     .withCursorControlEnabled(true)
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence)
                     .withRepeatDelayMs(1);
  await this.startFacegazeWithConfigAndForeheadLocation_(config, 0.1, 0.2);

  // Move the mouse.
  let result =
      new MockFaceLandmarkerResult().setNormalizedForeheadLocation(0.11, 0.21);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 360, y: 560});

  // Toggle (pause) FaceGaze.
  result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.9);
  this.processFaceLandmarkerResult(result);
  assertTrue(this.getFaceGaze().mouseController_.paused_);

  // Try to move the mouse.
  result = new MockFaceLandmarkerResult()
               .setNormalizedForeheadLocation(0.50, 0.50)
               .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0);
  this.processFaceLandmarkerResult(result);
  // Cursor position should remain the same.
  this.assertLatestCursorPosition({x: 360, y: 560});

  // Toggle (resume) FaceGaze.
  result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.9);
  // Don't trigger a mouse interval here because starting the MouseController
  // is an asynchronous operation and will cause flakes otherwise.
  this.processFaceLandmarkerResult(
      result, /*triggerMouseControllerInterval=*/ false);
  assertFalse(this.getFaceGaze().mouseController_.paused_);
  // Wait for the MouseController to fully start.
  await this.waitForValidMouseInterval();

  // TODO(b/330766904): Move the mouse physically and ensure FaceGaze starts
  // from that location when it's unpaused.
  // Try to move the mouse. Since the MouseController was just freshly
  // initialized, the cursor position won't change after processing this result.
  result = new MockFaceLandmarkerResult()
               .setNormalizedForeheadLocation(0.1, 0.2)
               .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 360, y: 560});

  // The second result should move the mouse.
  result = new MockFaceLandmarkerResult()
               .setNormalizedForeheadLocation(0.11, 0.21)
               .addGestureWithConfidence(MediapipeFacialGesture.JAW_OPEN, 0);
  this.processFaceLandmarkerResult(result);
  this.assertLatestCursorPosition({x: 120, y: 720});
});

AX_TEST_F('FaceGazeTest', 'KeyCombinations', async function() {
  const gestureToMacroName =
      new Map().set(FacialGesture.JAW_OPEN, MacroName.CUSTOM_KEY_COMBINATION);
  const gestureToConfidence = new Map().set(FacialGesture.JAW_OPEN, 0.7);
  const config = new Config()
                     .withMouseLocation({x: 600, y: 400})
                     .withGestureToMacroName(gestureToMacroName)
                     .withGestureToConfidence(gestureToConfidence);
  await this.configureFaceGaze(config);

  // Set the gestures to key combinations preference.
  const keyCombination = {
    key: KeyCode.C,
    modifiers: {ctrl: true},
  };
  await this.setPref(
      GestureHandler.GESTURE_TO_KEY_COMBO_PREF,
      {[FacialGesture.JAW_OPEN]: JSON.stringify(keyCombination)});

  // Verify that the preference propagated to FaceGaze.
  assertEquals(this.getFaceGaze().gestureHandler_.gesturesToKeyCombos_.size, 1);

  // Jaw open for custom key press.
  let result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.9);
  this.processFaceLandmarkerResult(result);
  let keyEvents = this.getKeyEvents();

  assertEquals(keyEvents.length, 1);
  assertEquals(
      keyEvents[0].type,
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN);
  assertEquals(keyEvents[0].keyCode, KeyCode.C);
  assertObjectEquals(keyEvents[0].modifiers, {ctrl: true});

  // Release jaw open for custom key release.
  result = new MockFaceLandmarkerResult().addGestureWithConfidence(
      MediapipeFacialGesture.JAW_OPEN, 0.1);
  this.processFaceLandmarkerResult(result);
  keyEvents = this.getKeyEvents();

  assertEquals(keyEvents.length, 2);
  assertEquals(
      keyEvents[1].type,
      chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYUP);
  assertEquals(keyEvents[1].keyCode, KeyCode.C);
  assertObjectEquals(keyEvents[1].modifiers, {ctrl: true});
});