chromium/chrome/browser/resources/chromeos/accessibility/common/testing/mock_speech_recognition_private.js

// 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.

/** @typedef {{transcript: string, isFinal: boolean}} */
let MockResultEvent;

/** @typedef {{message: string}} */
let MockErrorEvent;

/**
 * @typedef {{
 *   clientId: (number|undefined),
 *   locale: (string|undefined),
 *   interimResults: (boolean|undefined)
 * }}
 */
let MockStartOptions;

/**
 * @typedef {{
 *    clientId: (number|undefined)
 * }}
 */
let MockStopOptions;

/** @enum {string} */
const SpeechRecognitionType = {
  ON_DEVICE: 'onDevice',
  NETWORK: 'network',
};

/** A mock SpeechRecognitionPrivate API for tests. */
class MockSpeechRecognitionPrivate {
  /** @constructor */
  constructor() {
    // Properties.
    /** @private {boolean} */
    this.started_ = false;
    /** @private {!MockStartOptions}*/
    this.properties_ = {
      locale: undefined,
      interimResults: undefined,
    };
    /** @private {!SpeechRecognitionType} */
    this.speechRecognitionType_ = SpeechRecognitionType.NETWORK;

    // Event listeners.
    /** @private {?function({}):void} */
    this.onStopListener_ = null;
    /** @private {?function(!MockResultEvent):void} */
    this.onResultListener_ = null;
    /** @private {?function(!MockErrorEvent):void} */
    this.onErrorListener_ = null;

    // Mock events.

    /**
     * @type {!{
     *  addListener: function(Function):void
     *  removeListener: function(Function):void}}
     */
    this.onStop = {
      addListener: listener => {
        this.onStopListener_ = listener;
      },
      removeListener: listener => {
        if (this.onStopListener_ === listener) {
          this.onStopListener_ = null;
        }
      },
    };

    /**
     * @type {!{
     *  addListener: function(Function):void
     *  removeListener: function(Function):void}}
     */
    this.onResult = {
      addListener: listener => {
        this.onResultListener_ = listener;
      },
      removeListener: listener => {
        if (this.onResultListener_ === listener) {
          this.onResultListener_ = null;
        }
      },
    };

    /**
     * @type {!{
     *  addListener: function(Function):void
     *  removeListener: function(Function):void}}
     */
    this.onError = {
      addListener: listener => {
        this.onErrorListener_ = listener;
      },
      removeListener: listener => {
        if (this.onErrorListener_ === listener) {
          this.onErrorListener_ = null;
        }
      },
    };
  }

  // Mock methods.

  /**
   * @param {!MockStartOptions} props
   * @param {function(SpeechRecognitionType): void} callback
   */
  start(props, callback) {
    chrome.runtime.lastError = null;
    if (this.started_) {
      // If speech recognition is already active when calling start(), the real
      // API will set chrome.runtime.lastError. Do the same for the mock API.
      chrome.runtime.lastError = {
        message: 'Speech recognition already started',
      };
    }

    this.started_ = true;

    // The real API will update its properties when start() is called. Only
    // update properties that are specified by |props|.
    // Ignore `clientId`, since Dictation is the only client of this API.
    this.properties_.locale =
        props.locale !== undefined ? props.locale : this.properties_.locale;
    this.properties_.interimResults = props.interimResults !== undefined ?
        props.interimResults :
        this.properties_.interimResults;

    callback(this.speechRecognitionType_);
  }

  /**
   * @param {!MockStopOptions} props
   * @param {function():void} callback
   */
  stop(props, callback) {
    chrome.runtime.lastError = null;
    if (!this.started_) {
      // If speech recognition is already inactive when calling stop(), the real
      // API will set chrome.runtime.lastError. Do the same for the mock API.
      chrome.runtime.lastError = {
        message: 'Speech recognition already stopped',
      };
    }

    // The real API will run the callback and send an onStop event if speech
    // recognition was stopped by the API call.
    callback();
    if (this.started_) {
      this.fireMockStopEvent();
    }
  }

  // Methods for firing fake events.

  /**
   * @param {string} transcript
   * @param {boolean} isFinal
   */
  fireMockOnResultEvent(transcript, isFinal) {
    assertTrue(
        this.started_,
        'Speech recognition should be active when firing a result event');
    assertTrue(
        Boolean(this.onResultListener_),
        'Client should have added an onResult listener');

    // The real API will fire an onResult event.
    this.onResultListener_({transcript, isFinal});
  }

  /** Fires a fake stop event. */
  fireMockStopEvent() {
    assertTrue(
        this.started_,
        'Speech recognition should be active when firing a stop event');
    assertTrue(
        Boolean(this.onStopListener_),
        'Client should have added an onStop listener');

    // The real API will turn off speech recognition and fire an onStop event.
    this.started_ = false;
    this.onStopListener_({});
  }

  /** Fires a fake error event. */
  fireMockOnErrorEvent() {
    assertTrue(
        this.started_,
        'Speech recognition should be active when firing an error event');
    assertTrue(
        Boolean(this.onErrorListener_),
        'Client should have added an onError listener');

    // The real API will fire an onError and an onStop event.
    this.fireMockStopEvent();
    this.onErrorListener_({message: 'Speech recognition error'});
  }

  // Miscellaneous and helper methods.

  /**
   * The APIs properties are updated whenever start() is called. Updates
   * properties by calling start(), then stop().
   * @param {!MockStartOptions} props
   */
  updateProperties(props) {
    this.start(props, () => {});
    this.stop({}, () => {});
  }

  /** @return {boolean} */
  isStarted() {
    return this.started_ === true;
  }

  /** @return {string} */
  locale() {
    return this.properties_.locale;
  }

  /** @return {boolean} */
  interimResults() {
    return this.properties_.interimResults;
  }

  /** @param {!SpeechRecognitionType} type */
  setSpeechRecognitionType(type) {
    this.speechRecognitionType_ = type;
  }
}