chromium/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation_test.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.

GEN_INCLUDE(['dictation_test_base.js']);

/** Dictation feature using accessibility common extension browser tests. */
DictationE2ETest = class extends DictationE2ETestBase {};

AX_TEST_F('DictationE2ETest', 'ResetsImeAfterToggleOff', async function() {
  // Set something as the active IME.
  this.mockInputMethodPrivate.setCurrentInputMethod('keyboard_cat');
  this.mockLanguageSettingsPrivate.addInputMethod('keyboard_cat');
  this.toggleDictationOn();
  this.toggleDictationOff();
  this.checkDictationImeInactive('keyboard_cat');
});

AX_TEST_F('DictationE2ETest', 'UpdateRecognitionProps', async function() {
  assertFalse(this.getSpeechRecognitionActive());
  assertEquals(undefined, this.getSpeechRecognitionLocale());
  assertEquals(undefined, this.getSpeechRecognitionInterimResults());

  const locale = await this.getPref(Dictation.DICTATION_LOCALE_PREF);
  this.updateSpeechRecognitionProperties(
      {locale: locale.value, interimResults: true});

  assertEquals(locale.value, this.getSpeechRecognitionLocale());
  assertTrue(this.getSpeechRecognitionInterimResults());
});

AX_TEST_F(
    'DictationE2ETest', 'UpdatesSpeechRecognitionLangOnLocaleChange',
    async function() {
      let locale = await this.getPref(Dictation.DICTATION_LOCALE_PREF);
      this.updateSpeechRecognitionProperties({locale: locale.value});
      assertEquals(locale.value, this.getSpeechRecognitionLocale());
      // Change the locale.
      await this.setPref(Dictation.DICTATION_LOCALE_PREF, 'es-ES');
      locale = await this.getPref(Dictation.DICTATION_LOCALE_PREF);
      this.updateSpeechRecognitionProperties({locale: locale.value});
      assertEquals('es-ES', this.getSpeechRecognitionLocale());
    });

AX_TEST_F('DictationE2ETest', 'StopsOnRecognitionError', async function() {
  this.toggleDictationOn();
  this.sendSpeechRecognitionErrorEvent();
  assertFalse(this.getDictationActive());
  assertFalse(this.getSpeechRecognitionActive());
});

AX_TEST_F('DictationE2ETest', 'StopsOnImeBlur', async function() {
  this.toggleDictationOn();
  this.blurInputContext();
  assertFalse(this.getSpeechRecognitionActive());
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F('DictationE2ETest', 'CommitsFinalResults', async function() {
  this.toggleDictationOn();
  this.sendInterimSpeechResult('kittens');
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
  assertTrue(this.getDictationActive());

  this.mockInputIme.clearLastParameters();
  this.sendFinalSpeechResult('kittens!');
  await this.assertCommittedText('kittens!');
  assertTrue(this.getDictationActive());

  this.mockInputIme.clearLastParameters();
  this.sendFinalSpeechResult('puppies!');
  await this.assertCommittedText('puppies!');
  assertTrue(this.getDictationActive());

  this.mockInputIme.clearLastParameters();
  this.toggleDictationOff();
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F(
    'DictationE2ETest', 'CommitsInterimResultsWhenRecognitionStops',
    async function() {
      this.toggleDictationOn();
      this.sendInterimSpeechResult('fish fly');
      this.sendSpeechRecognitionStopEvent();
      assertFalse(this.getDictationActive());
      await this.assertCommittedText('fish fly');
    });

AX_TEST_F(
    'DictationE2ETest', 'DoesNotCommitInterimResultsAfterImeBlur',
    async function() {
      this.toggleDictationOn();
      this.sendInterimSpeechResult('ducks dig');
      this.blurInputContext();
      assertFalse(this.getDictationActive());
      assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
    });

AX_TEST_F('DictationE2ETest', 'TimesOutWithNoImeContext', async function() {
  this.mockSetTimeoutMethod();
  this.toggleDictationOn();

  const callback =
      this.getCallbackWithDelay(Dictation.Timeouts.NO_FOCUSED_IME_MS);
  assertNotNullNorUndefined(callback);

  // Triggering the timeout should cause a request to toggle Dictation, but
  // nothing should be committed after AccessibilityPrivate toggle is received.
  callback();
  this.clearSetTimeoutData();
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F('DictationE2ETest', 'TimesOutWithNoSpeechNetwork', async function() {
  this.mockSpeechRecognitionPrivate.setSpeechRecognitionType(
      SpeechRecognitionType.NETWORK);
  this.mockSetTimeoutMethod();
  this.toggleDictationOn();

  const callback =
      this.getCallbackWithDelay(Dictation.Timeouts.NO_SPEECH_NETWORK_MS);
  assertNotNullNorUndefined(callback);

  // Triggering the timeout should cause a request to toggle Dictation, but
  // nothing should be committed after AccessibilityPrivate toggle is received.
  callback();
  this.clearSetTimeoutData();
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F('DictationE2ETest', 'TimesOutWithNoSpeechOnDevice', async function() {
  this.mockSpeechRecognitionPrivate.setSpeechRecognitionType(
      SpeechRecognitionType.ON_DEVICE);
  this.mockSetTimeoutMethod();
  this.toggleDictationOn();

  const callback =
      this.getCallbackWithDelay(Dictation.Timeouts.NO_SPEECH_ONDEVICE_MS);
  assertNotNullNorUndefined(callback);

  // Triggering the timeout should cause a request to toggle Dictation, but
  // nothing should be committed after AccessibilityPrivate toggle is received.
  callback();
  this.clearSetTimeoutData();
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F(
    'DictationE2ETest', 'TimesOutAfterInterimResultsAndCommits',
    async function() {
      this.mockSetTimeoutMethod();
      this.toggleDictationOn();
      this.sendInterimSpeechResult('sheep sleep');
      this.mockInputIme.clearLastParameters();

      // The timeout should be set based on the interim result.
      const callback =
          this.getCallbackWithDelay(Dictation.Timeouts.NO_NEW_SPEECH_MS);
      assertNotNullNorUndefined(callback);

      // Triggering the timeout should cause a request to toggle Dictation, and
      // after AccessibilityPrivate calls toggleDictation, to commit the interim
      // text.
      callback();
      this.clearSetTimeoutData();
      assertFalse(this.getDictationActive());
      await this.assertCommittedText('sheep sleep');
    });

AX_TEST_F('DictationE2ETest', 'TimesOutAfterFinalResults', async function() {
  this.mockSetTimeoutMethod();
  this.toggleDictationOn();
  this.sendFinalSpeechResult('bats bounce');
  await this.assertCommittedText('bats bounce');
  this.mockInputIme.clearLastParameters();

  // The timeout should be set based on the final result.
  const callback =
      this.getCallbackWithDelay(Dictation.Timeouts.NO_SPEECH_NETWORK_MS);

  // Triggering the timeout should stop listening.
  callback();
  this.clearSetTimeoutData();
  assertFalse(this.getDictationActive());
  assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
});

AX_TEST_F(
    'DictationE2ETest', 'CommandsDoNotCommitThemselves', async function() {
      this.toggleDictationOn();
      for (const command of Object.values(this.commandStrings)) {
        this.sendInterimSpeechResult(command);
        if (command !== this.commandStrings.LIST_COMMANDS) {
          // LIST_COMMANDS opens a new tab and ends Dictation. Skip this.
          this.sendFinalSpeechResult(command);
        }
        if (command === this.commandStrings.NEW_LINE) {
          await this.assertCommittedText('\n');
          this.mockInputIme.clearLastParameters();
        } else {
          // On final result, nothing is committed; instead, an action is taken.
          assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
        }

        // Try to type the command e.g. "type delete".
        // The command should be entered but not the word "type".
        this.sendFinalSpeechResult(`type ${command}`);
        await this.assertCommittedText(command);
        this.mockInputIme.clearLastParameters();
      }
    });

AX_TEST_F(
    'DictationE2ETest', 'TypePrefixWorksForNonCommands', async function() {
      this.toggleDictationOn();
      this.sendFinalSpeechResult('type this is a test');
      await this.assertCommittedText('this is a test');
    });

AX_TEST_F(
    'DictationE2ETest', 'DontCommitAfterMacroSuccess', async function() {
      this.toggleDictationOn();
      this.sendInterimSpeechResult('move to the next line');
      // Perform the next line command.
      this.sendFinalSpeechResult('move to the next line');
      // Wait for the UI to show macro success.
      await this.waitForUIProperties({
        visible: true,
        icon: this.iconType.MACRO_SUCCESS,
        text: this.commandStrings.NAV_NEXT_LINE,
      });
      this.toggleDictationOff();
      // No text should be committed.
      assertFalse(Boolean(this.mockInputIme.getLastCommittedParameters()));
    });


AX_TEST_F('DictationE2ETest', 'NoCommandsWhenNotSupported', async function() {
  this.toggleDictationOn();
  this.sendFinalSpeechResult('New line');
  await this.assertCommittedText('\n');
  this.mockInputIme.clearLastParameters();

  // System language is en-US. If the Dictation locale doesn't match,
  // commands should not work.
  await this.setPref(Dictation.DICTATION_LOCALE_PREF, 'es-ES');

  // Now this text should just get typed in instead of reinterpreted.
  this.sendFinalSpeechResult('New line');
  await this.assertCommittedText('New line');
  this.mockInputIme.clearLastParameters();
});

// TODO(crbug.com/1442591) flaky test
AX_TEST_F(
    'DictationE2ETest', 'DISABLED_SilencesSpokenFeedbackWhenStarting',
    async function() {
      assertEquals(
          0, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());

      // Turn on ChromeVox
      await this.setPref(Dictation.SPOKEN_FEEDBACK_PREF, true);

      // Now silenceSpokenFeedback should get called when toggling Dictation on.
      this.toggleDictationOn();
      assertEquals(
          1, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());

      // It should not be called when turning Dictation off.
      this.toggleDictationOff();
      assertEquals(
          1, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());
    });

AX_TEST_F(
    'DictationE2ETest', 'DoesNotSilenceSpokenFeedbackUnnecessarily',
    async function() {
      assertEquals(
          0, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());

      // Check that when ChromeVox is disabled we don't try to silence it when
      // Dictation gets toggled.
      this.toggleDictationOn();
      assertEquals(
          0, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());
      this.toggleDictationOff();
      assertEquals(
          0, this.mockAccessibilityPrivate.getSpokenFeedbackSilencedCount());
    });

AX_TEST_F(
    'DictationE2ETest', 'SurroundingInfoResetsAfterToggleOff',
    async function() {
      assertEquals(null, this.getInputController().surroundingInfo_);
      this.toggleDictationOn();
      const value = 'This is a test';
      this.sendFinalSpeechResult(value);
      // A surroundingTextChanged event is fired whenever the editable value
      // or the caret index is changed.
      this.mockInputIme.callOnSurroundingTextChanged({
        anchor: value.length,
        focus: value.length,
        offset: 0,
        text: value,
      });
      assertNotNullNorUndefined(this.getInputController().surroundingInfo_);
      this.toggleDictationOff();
      assertEquals(null, this.getInputController().surroundingInfo_);
    });

AX_TEST_F('DictationE2ETest', 'ShowsToastWhenMicMuted', async function() {
  const StreamType = chrome.audio.StreamType;
  const ToastType = chrome.accessibilityPrivate.ToastType;

  assertEquals(
      0,
      this.mockAccessibilityPrivate.getShowToastCount(
          ToastType.DICTATION_MIC_MUTED));
  await new Promise(
      resolve =>
          chrome.audio.setMute(StreamType.INPUT, /*isMuted=*/ true, resolve));

  // Use callOnToggleDictation instead of toggleDictation because the latter
  // asserts that speech recognition successfully starts, which won't be the
  // case here.
  this.mockAccessibilityPrivate.callOnToggleDictation(true);
  assertTrue(this.getDictationActive());
  this.checkDictationImeActive();
  // Focus the input context so that we try to start speech recognition. We'll
  // fail to start since the device is muted.
  this.focusInputContext();
  // Confirm that a toast was shown and that Dictation is inactive.
  assertEquals(
      1,
      this.mockAccessibilityPrivate.getShowToastCount(
          ToastType.DICTATION_MIC_MUTED));
  assertFalse(this.getDictationActive());
});