Commit b54f00f2 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

ChromeVox Tutorial: Automatic reading behavior.

This change implements automatic reading behavior for the ChromeVox
tutorial. This change implements the following:

1. When an interactive lesson is shown, read the lesson title. This
is needed because we focus the text content for these lessons.
2. When a non-interactive lesson is shown, read the lesson contents.
3. Refactor requestSpeech() to pass queuemode and properties
for more control over speech behavior.
4. Add @suppresss statements to a few functions. This allows
us to reference the panel window without triggering closure errors.

Automated tests are included confirm bullets 1 and 2.

Fixed: 1129194
Change-Id: I2ce38fc0228bce749033f4effac4b79846dd696f
AX-Relnotes: N/A
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2414776
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812503}
parent 412b99ac
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/**
* @fileoverview Defines a custom Polymer component for the ChromeVox
* interactive tutorial engine.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js'; import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js'; import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
...@@ -570,9 +575,15 @@ Polymer({ ...@@ -570,9 +575,15 @@ Polymer({
// update their visibility. // update their visibility.
this.activeLessonNum = this.includedLessons[index].lessonNum; this.activeLessonNum = this.includedLessons[index].lessonNum;
const lesson = this.includedLessons[this.activeLessonIndex]; const lesson = this.getCurrentLesson();
if (lesson.autoInteractive) { if (lesson.autoInteractive) {
this.startInteractiveMode(lesson.actions); this.startInteractiveMode(lesson.actions);
// Read the title since initial focus gets placed on the first piece of
// text content.
this.readCurrentLessonTitle();
} else {
// Otherwise, automatically read current lesson content.
setTimeout(this.readCurrentLessonContent.bind(this), 1000);
} }
}, },
...@@ -830,6 +841,8 @@ Polymer({ ...@@ -830,6 +841,8 @@ Polymer({
/** /**
* @param {NudgeType} type * @param {NudgeType} type
* @private * @private
* @suppress {undefinedVars|missingProperties} For referencing QueueMode,
* which is defined on the Panel window.
*/ */
initializeNudges(type) { initializeNudges(type) {
const maybeGiveNudge = (msg) => { const maybeGiveNudge = (msg) => {
...@@ -839,7 +852,7 @@ Polymer({ ...@@ -839,7 +852,7 @@ Polymer({
return; return;
} }
this.requestSpeech(msg); this.requestSpeech(msg, QueueMode.INTERJECT);
}; };
this.nudgeArray = []; this.nudgeArray = [];
...@@ -848,7 +861,8 @@ Polymer({ ...@@ -848,7 +861,8 @@ Polymer({
// strings. // strings.
const hints = this.lessonData[this.activeLessonNum].hints; const hints = this.lessonData[this.activeLessonNum].hints;
for (const hint of hints) { for (const hint of hints) {
this.nudgeArray.push(this.requestSpeech.bind(this, hint)); this.nudgeArray.push(
this.requestSpeech.bind(this, hint, QueueMode.INTERJECT));
} }
} else if (type === NudgeType.GENERAL) { } else if (type === NudgeType.GENERAL) {
this.nudgeArray = [ this.nudgeArray = [
...@@ -860,7 +874,8 @@ Polymer({ ...@@ -860,7 +874,8 @@ Polymer({
maybeGiveNudge.bind( maybeGiveNudge.bind(
this, 'Hint: Press Search + Space to activate the current item.'), this, 'Hint: Press Search + Space to activate the current item.'),
this.requestSpeech.bind( this.requestSpeech.bind(
this, 'Hint: Press Escape if you would like to exit this tutorial.') this, 'Hint: Press Escape if you would like to exit this tutorial.',
QueueMode.INTERJECT)
]; ];
} else { } else {
throw new Error('Invalid NudgeType: ' + type); throw new Error('Invalid NudgeType: ' + type);
...@@ -894,11 +909,16 @@ Polymer({ ...@@ -894,11 +909,16 @@ Polymer({
/** /**
* @param {string} text * @param {string} text
* @param {number} queueMode
* @param {{doNotInterrupt: boolean}=} properties
* @private * @private
* @suppress {undefinedVars|missingProperties} For referencing QueueMode,
* which is defined on the Panel window.
*/ */
requestSpeech(text) { requestSpeech(text, queueMode, properties) {
this.dispatchEvent( this.dispatchEvent(new CustomEvent(
new CustomEvent('requestspeech', {composed: true, detail: {text}})); 'requestspeech',
{composed: true, detail: {text, queueMode, properties}}));
}, },
/** @private */ /** @private */
...@@ -919,5 +939,34 @@ Polymer({ ...@@ -919,5 +939,34 @@ Polymer({
} }
this.restartNudges(); this.restartNudges();
},
/** @return {!TutorialLesson} */
getCurrentLesson() {
return this.includedLessons[this.activeLessonIndex];
},
/**
* @private
* @suppress {undefinedVars|missingProperties} For referencing QueueMode,
* which is defined on the Panel window.
*/
readCurrentLessonTitle() {
const lesson = this.getCurrentLesson();
this.requestSpeech(
lesson.title, QueueMode.INTERJECT, {doNotInterrupt: true});
},
/**
* @private
* @suppress {undefinedVars|missingProperties} For referencing QueueMode,
* which is defined on the Panel window.
*/
readCurrentLessonContent() {
const lesson = this.getCurrentLesson();
for (const text of lesson.content) {
// Queue lesson content so it is read after the lesson title.
this.requestSpeech(text, QueueMode.QUEUE);
}
} }
}); });
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/**
* @fileoverview Defines a custom Polymer component for a lesson in the
* ChromeVox interactive tutorial.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js'; import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js'; import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
...@@ -232,11 +237,12 @@ export const TutorialLesson = Polymer({ ...@@ -232,11 +237,12 @@ export const TutorialLesson = Polymer({
}, },
/** /**
* Requests speech from the Panel.
* @param {string} text * @param {string} text
* @private * @private
*/ */
requestSpeech(text) { requestSpeech(text) {
// TODO (akihiroota): Migrate this to i_tutorial.js so that the tutorial
// engine controls all speech requests.
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('requestspeech', {composed: true, detail: {text}})); new CustomEvent('requestspeech', {composed: true, detail: {text}}));
}, },
......
...@@ -24,6 +24,7 @@ goog.require('Msgs'); ...@@ -24,6 +24,7 @@ goog.require('Msgs');
goog.require('PanelCommand'); goog.require('PanelCommand');
goog.require('PanelMenu'); goog.require('PanelMenu');
goog.require('PanelMenuItem'); goog.require('PanelMenuItem');
goog.require('QueueMode');
goog.require('Tutorial'); goog.require('Tutorial');
goog.require('UserAnnotationHandler'); goog.require('UserAnnotationHandler');
...@@ -1191,11 +1192,24 @@ Panel = class { ...@@ -1191,11 +1192,24 @@ Panel = class {
Panel.onCloseTutorial(); Panel.onCloseTutorial();
}); });
$('i-tutorial').addEventListener('requestspeech', (evt) => { $('i-tutorial').addEventListener('requestspeech', (evt) => {
const text = evt.detail.text;
const background = chrome.extension.getBackgroundPage(); const background = chrome.extension.getBackgroundPage();
/**
* @type {{
* text: string,
* queueMode: QueueMode,
* properties: ({doNotInterrupt: boolean}|undefined)}}
*/
const detail = evt.detail;
const text = detail.text;
const queueMode = detail.queueMode;
const properties = detail.properties || {};
if (!text || queueMode === undefined) {
throw new Error(
`Must specify text and queueMode when requesting speech from the
tutorial`);
}
const cvox = background['ChromeVox']; const cvox = background['ChromeVox'];
cvox.tts.speak( cvox.tts.speak(text, queueMode, properties);
text, background.QueueMode.INTERJECT, {'doNotInterrupt': true});
}); });
$('i-tutorial').addEventListener('startinteractivemode', (evt) => { $('i-tutorial').addEventListener('startinteractivemode', (evt) => {
const actions = evt.detail.actions; const actions = evt.detail.actions;
......
...@@ -395,3 +395,53 @@ TEST_F('ChromeVoxTutorialTest', 'NextPreviousButtons', function() { ...@@ -395,3 +395,53 @@ TEST_F('ChromeVoxTutorialTest', 'NextPreviousButtons', function() {
.replay(); .replay();
}); });
}); });
// Tests that the title of an interactive lesson is read when shown.
TEST_F('ChromeVoxTutorialTest', 'AutoReadTitle', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.simpleDoc, async function(root) {
await this.launchAndWaitForTutorial();
const tutorial = this.getPanel().iTutorial;
mockFeedback.expectSpeech('Choose your tutorial experience')
.call(doCmd('nextObject'))
.expectSpeech('Quick orientation', 'Button')
.call(doCmd('forceClickOnCurrentItem'))
.expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
.call(doCmd('nextObject'))
.expectSpeech('Welcome to ChromeVox!', 'Button')
.call(doCmd('forceClickOnCurrentItem'))
.expectSpeech('Welcome to ChromeVox!')
.expectSpeech(
'Welcome to the ChromeVox tutorial. To exit this tutorial at any ' +
'time, press the Escape key on the top left corner of the ' +
'keyboard. To turn off ChromeVox, hold Control and Alt, and ' +
`press Z. When you're ready, use the spacebar to move to the ` +
'next lesson.')
.replay();
});
});
// Tests that the content of a non-interactive lesson is read when shown.
TEST_F('ChromeVoxTutorialTest', 'AutoReadLesson', function() {
const mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(this.simpleDoc, async function(root) {
await this.launchAndWaitForTutorial();
const tutorial = this.getPanel().iTutorial;
mockFeedback.expectSpeech('Choose your tutorial experience')
.call(doCmd('nextObject'))
.expectSpeech('Quick orientation', 'Button')
.call(doCmd('nextObject'))
.expectSpeech('Essential keys', 'Button')
.call(doCmd('forceClickOnCurrentItem'))
.expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
.call(() => {
tutorial.showLesson(0);
})
.expectSpeech('On, Off, and Stop', 'Heading 1')
.expectSpeech(
'To temporarily stop ChromeVox from speaking, ' +
'press the Control key.')
.expectSpeech('To turn ChromeVox on or off, use Control+Alt+Z.')
.replay();
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment