blob: 6a67411573a5b3d5e592a6eb59fc717cc23564c9 [file] [log] [blame]
// 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(['../../common/testing/e2e_test_base.js']);
GEN_INCLUDE(['../../common/testing/mock_accessibility_private.js']);
GEN_INCLUDE(['../../common/testing/mock_input_ime.js']);
GEN_INCLUDE(['../../common/testing/mock_input_method_private.js']);
GEN_INCLUDE(['../../common/testing/mock_language_settings_private.js']);
GEN_INCLUDE(['../../common/testing/mock_speech_recognition_private.js']);
/**
* @typedef {{
* name: (string|undefined),
* repeat: (number|undefined),
* smart: (boolean|undefined),
* }}
*/
let ParseTestExpectations;
/** A class that represents a test case for parsing text. */
class ParseTestCase {
/**
* @param {string} text The text to be parsed
* @param {!ParseTestExpectations} expectations
* @constructor
*/
constructor(text, expectations) {
/** @type {string} */
this.text = text;
/** @type {string|undefined} */
this.expectedName = expectations.name;
/** @type {number|undefined} */
this.expectedRepeat = expectations.repeat;
/** @type {boolean|undefined} */
this.expectedSmart = expectations.smart;
}
}
/**
* Base class for tests for Dictation feature using accessibility common
* extension browser tests.
*/
DictationE2ETestBase = class extends E2ETestBase {
constructor() {
super();
this.navigateLacrosWithAutoComplete = true;
this.mockAccessibilityPrivate = new MockAccessibilityPrivate();
this.iconType = this.mockAccessibilityPrivate.DictationBubbleIconType;
this.hintType = this.mockAccessibilityPrivate.DictationBubbleHintType;
chrome.accessibilityPrivate = this.mockAccessibilityPrivate;
this.mockInputIme = MockInputIme;
chrome.input.ime = this.mockInputIme;
this.mockInputMethodPrivate = MockInputMethodPrivate;
chrome.inputMethodPrivate = this.mockInputMethodPrivate;
this.mockLanguageSettingsPrivate = MockLanguageSettingsPrivate;
chrome.languageSettingsPrivate = this.mockLanguageSettingsPrivate;
this.mockSpeechRecognitionPrivate = new MockSpeechRecognitionPrivate();
chrome.speechRecognitionPrivate = this.mockSpeechRecognitionPrivate;
this.dictationEngineId =
'_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
/** @private {!Array<Object{delay: number, callback: Function}} */
this.setTimeoutData_ = [];
/** @private {number} */
this.imeContextId_ = 1;
this.commandStrings = {
DELETE_PREV_CHAR: 'delete',
NAV_PREV_CHAR: 'move to the previous character',
NAV_NEXT_CHAR: 'move to the next character',
NAV_PREV_LINE: 'move to the previous line',
NAV_NEXT_LINE: 'move to the next line',
COPY_SELECTED_TEXT: 'copy',
PASTE_TEXT: 'paste',
CUT_SELECTED_TEXT: 'cut',
UNDO_TEXT_EDIT: 'undo',
REDO_ACTION: 'redo',
SELECT_ALL_TEXT: 'select all',
UNSELECT_TEXT: 'unselect',
LIST_COMMANDS: 'help',
NEW_LINE: 'new line',
};
// Re-initialize AccessibilityCommon with mock APIs.
const reinit = module => {
accessibilityCommon = new module.AccessibilityCommon();
};
import('/accessibility_common/accessibility_common_loader.js').then(reinit);
}
/** @override */
async setUpDeferred() {
await super.setUpDeferred();
// Wait for the Dictation module to load and set the Dictation locale.
await importModule(
'Dictation', '/accessibility_common/dictation/dictation.js');
assertNotNullNorUndefined(Dictation);
await importModule(
'LocaleInfo', '/accessibility_common/dictation/locale_info.js');
await new Promise(resolve => {
chrome.accessibilityFeatures.dictation.set({value: true}, resolve);
});
await this.setPref(Dictation.DICTATION_LOCALE_PREF, 'en-US');
// By default, Dictation JS tests should use regex parsing.
accessibilityCommon.dictation_.disablePumpkinForTesting();
// Increase Dictation's NO_FOCUSED_IME timeout to reduce flakiness on slower
// builds.
accessibilityCommon.dictation_.increaseNoFocusedImeTimeoutForTesting();
}
/** @override */
testGenCppIncludes() {
super.testGenCppIncludes();
GEN(`
#include "ash/accessibility/accessibility_delegate.h"
#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/command_line.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "ui/accessibility/accessibility_features.h"
#include "components/prefs/pref_service.h"
#include "ash/constants/ash_pref_names.h"
`);
}
/** @override */
testGenPreamble() {
super.testGenPreamble();
GEN(`
browser()->profile()->GetPrefs()->SetBoolean(
ash::prefs::kDictationAcceleratorDialogHasBeenAccepted, true);
base::OnceClosure load_cb =
base::BindOnce(&ash::AccessibilityManager::SetDictationEnabled,
base::Unretained(ash::AccessibilityManager::Get()),
true);
`);
// Allow informational Pumpkin messages.
super.testGenPreambleCommon(
/*extensionIdName=*/ 'kAccessibilityCommonExtensionId',
/*failOnConsoleError=*/ true,
/*allowedMessages=*/[
'Pumpkin installed, but data is empty',
`wasm streaming compile failed: TypeError: Failed to execute ` +
`'compile' on 'WebAssembly': Incorrect response MIME type. ` +
`Expected 'application/wasm'.`,
'falling back to ArrayBuffer instantiation',
'Pumpkin module loaded.',
`Unchecked runtime.lastError: Couldn't retrieve Pumpkin data.`,
]);
}
/** Turns on Dictation and checks IME and Speech Recognition state. */
toggleDictationOn() {
this.mockAccessibilityPrivate.callOnToggleDictation(true);
assertTrue(this.getDictationActive());
this.checkDictationImeActive();
this.focusInputContext();
assertTrue(this.getSpeechRecognitionActive());
}
/**
* Turns Dictation off and checks IME and Speech Recognition state. Note that
* Dictation can also be toggled off by blurring the current input context,
* Speech recognition errors, or timeouts.
*/
toggleDictationOff() {
this.mockAccessibilityPrivate.callOnToggleDictation(false);
assertFalse(
this.getDictationActive(),
'Dictation should be inactive after toggling Dictation');
this.checkDictationImeInactive();
assertFalse(
this.getSpeechRecognitionActive(), 'Speech recognition should be off');
}
/** Checks that Dictation is the active IME. */
checkDictationImeActive() {
assertEquals(
this.dictationEngineId,
this.mockInputMethodPrivate.getCurrentInputMethodForTest());
assertTrue(this.mockLanguageSettingsPrivate.hasInputMethod(
this.dictationEngineId));
}
/*
* Checks that Dictation is not the active IME.
* @param {*} opt_activeImeId If we do not expect Dictation IME to be
* activated, an optional IME ID that we do expect to be activated.
*/
checkDictationImeInactive(opt_activeImeId) {
assertNotEquals(
this.dictationEngineId,
this.mockInputMethodPrivate.getCurrentInputMethodForTest());
assertFalse(this.mockLanguageSettingsPrivate.hasInputMethod(
this.dictationEngineId));
if (opt_activeImeId) {
assertEquals(
opt_activeImeId,
this.mockInputMethodPrivate.getCurrentInputMethodForTest());
}
}
// Timeout methods.
mockSetTimeoutMethod() {
setTimeout = (callback, delay) => {
// setTimeout can be called from several different sources, so track
// them using an Array.
this.setTimeoutData_.push({delay, callback});
};
}
/** @return {?Function} */
getCallbackWithDelay(delay) {
for (const data of this.setTimeoutData_) {
if (data.delay === delay) {
return data.callback;
}
}
return null;
}
clearSetTimeoutData() {
this.setTimeoutData_ = [];
}
// Ime methods.
focusInputContext() {
this.mockInputIme.callOnFocus(this.imeContextId_);
}
blurInputContext() {
this.mockInputIme.callOnBlur(this.imeContextId_);
}
/**
* Checks that the latest IME commit text matches the expected value.
* @param {string} expected
* @return {!Promise}
*/
async assertCommittedText(expected) {
if (!this.mockInputIme.getLastCommittedParameters()) {
await this.mockInputIme.waitForCommit();
}
assertEquals(expected, this.mockInputIme.getLastCommittedParameters().text);
assertEquals(
this.imeContextId_,
this.mockInputIme.getLastCommittedParameters().contextID);
}
// Getters and setters.
/** @return {boolean} */
getDictationActive() {
return this.mockAccessibilityPrivate.getDictationActive();
}
/**
* Async function to get a preference value from Settings.
* @param {string} name
* @return {!Promise<*>}
*/
async getPref(name) {
return new Promise(resolve => {
chrome.settingsPrivate.getPref(name, ret => {
resolve(ret);
});
});
}
/**
* Async function to set a preference value in Settings.
* @param {string} name
* @return {!Promise}
*/
async setPref(name, value) {
return new Promise(resolve => {
chrome.settingsPrivate.setPref(name, value, undefined, () => {
resolve();
});
});
}
/** @return {InputTextStrategy} */
getInputTextStrategy() {
return accessibilityCommon.dictation_.speechParser_.inputTextStrategy_;
}
/** @return {SimpleParseStrategy} */
getSimpleParseStrategy() {
return accessibilityCommon.dictation_.speechParser_.simpleParseStrategy_;
}
/** @return {PumpkinParseStrategy} */
getPumpkinParseStrategy() {
return accessibilityCommon.dictation_.speechParser_.pumpkinParseStrategy_;
}
/** @return {InputController} */
getInputController() {
return accessibilityCommon.dictation_.inputController_;
}
// Speech recognition methods.
/** @param {string} transcript */
sendInterimSpeechResult(transcript) {
this.mockSpeechRecognitionPrivate.fireMockOnResultEvent(
transcript, /*is_final=*/ false);
}
/** @param {string} transcript */
sendFinalSpeechResult(transcript) {
this.mockSpeechRecognitionPrivate.fireMockOnResultEvent(
transcript, /*is_final=*/ true);
}
sendSpeechRecognitionErrorEvent() {
this.mockSpeechRecognitionPrivate.fireMockOnErrorEvent();
}
sendSpeechRecognitionStopEvent() {
this.mockSpeechRecognitionPrivate.fireMockStopEvent();
}
/** @return {boolean} */
getSpeechRecognitionActive() {
return this.mockSpeechRecognitionPrivate.isStarted();
}
/** @return {string|undefined} */
getSpeechRecognitionLocale() {
return this.mockSpeechRecognitionPrivate.locale();
}
/** @return {boolean|undefined} */
getSpeechRecognitionInterimResults() {
return this.mockSpeechRecognitionPrivate.interimResults();
}
/**
* @param {{
* clientId: (number|undefined),
* locale: (string|undefined),
* interimResults: (boolean|undefined)
* }} properties
*/
updateSpeechRecognitionProperties(properties) {
this.mockSpeechRecognitionPrivate.updateProperties(properties);
}
// UI-related methods.
/**
* Waits for the updateDictationBubble() API to be called with the given
* properties.
* @param {DictationBubbleProperties} targetProps
* @return {!Promise}
*/
async waitForUIProperties(targetProps) {
if (this.uiPropertiesMatch_(targetProps)) {
return;
}
await new Promise(resolve => {
const onUpdateDictationBubble = () => {
if (this.uiPropertiesMatch_(targetProps)) {
this.mockAccessibilityPrivate.removeUpdateDictationBubbleListener();
resolve();
}
};
this.mockAccessibilityPrivate.addUpdateDictationBubbleListener(
onUpdateDictationBubble);
});
}
/**
* Returns true if `targetProps` matches the most recent UI properties. Must
* match exactly.
* @param {DictationBubbleProperties} targetProps
* @return {boolean}
* @private
*/
uiPropertiesMatch_(targetProps) {
/** @type {function(!Array<string>,!Array<string>) : boolean} */
const areEqual = (arr1, arr2) => {
return arr1.every((val, index) => val === arr2[index]);
};
const actualProps = this.mockAccessibilityPrivate.getDictationBubbleProps();
if (!actualProps) {
return false;
}
if (Object.keys(actualProps).length !== Object.keys(targetProps).length) {
return false;
}
for (const key of Object.keys(targetProps)) {
if (Array.isArray(targetProps[key]) && Array.isArray(actualProps[key])) {
// For arrays, ensure that we compare the contents of the arrays.
if (!areEqual(targetProps[key], actualProps[key])) {
return false;
}
} else if (targetProps[key] !== actualProps[key]) {
return false;
}
}
return true;
}
/**
* Always allows Dictation commands, even if the Dictation locale and browser
* locale differ. Only used for testing.
*/
alwaysEnableCommands() {
LocaleInfo.alwaysEnableCommandsForTesting = true;
}
/**
* @param {!ParseTestCase} testCase
* @return {!Promise}
*/
async runInputTextParseTestCase(testCase) {
const macro = await this.getInputTextStrategy().parse(testCase.text);
this.runParseTestCaseAssertions(testCase, macro);
}
/**
* @param {!ParseTestCase} testCase
* @return {!Promise}
*/
async runSimpleParseTestCase(testCase) {
const macro = await this.getSimpleParseStrategy().parse(testCase.text);
this.runParseTestCaseAssertions(testCase, macro);
}
/**
* @param {!ParseTestCase} testCase
* @return {!Promise}
*/
async runPumpkinParseTestCase(testCase) {
const macro = await this.getPumpkinParseStrategy().parse(testCase.text);
this.runParseTestCaseAssertions(testCase, macro);
}
/**
* @param {!ParseTestCase} testCase
* @param {?Macro} macro
*/
runParseTestCaseAssertions(testCase, macro) {
const expectedName = testCase.expectedName;
const expectedRepeat = testCase.expectedRepeat;
const expectedSmart = testCase.expectedSmart;
if (!macro) {
assertEquals(undefined, expectedName);
assertEquals(undefined, expectedRepeat);
assertEquals(undefined, expectedSmart);
return;
}
if (expectedName) {
assertEquals(expectedName, macro.getNameAsString());
}
if (expectedRepeat) {
assertEquals(expectedRepeat, macro.repeat_);
}
if (expectedSmart) {
assertEquals(expectedSmart, macro.isSmart());
}
}
};