Commit c8fb7466 authored by Lei Shi's avatar Lei Shi Committed by Chromium LUCI CQ

Address the issue of invalidated nodes in Select to Speak

When a user resize or change a webpage, the selected inline nodes may be
invalidated. To address this issue, we reuse the prior STS logic, which
converts all selected content into node groups. Then, we send the node
groups to TTS one by one. The only entry point for reading new content
will be startSpeechQueue_, which uses updateNodeGroups_ to convert nodes
into node groups, and start TTS using startCurrentNodeGroup_.

This CL also refactored the names of variables and removed unnecessary
variables. The prior CLs (http://crrev.com/c/2582524,
http://crrev.com/c/2587799, and http://crrev.com/c/2587820) have
modified navigation features so that they do not rely on
navigationState_ anymore.Thus I implement the following rename and
refactor:
1. Rename this.currentNode_ to this.currentNodeGroupItem_ since the
variable is actually for a node group item.
2. Rename this.currentNodeGroupIndex_ to this.currentNodeGroupItemIndex_
since the variable is meant to be the index for node group item.
3. Add this.currentNodeGroups_ and this.currentNodeGroupIndex_, which
are used for tracking the selected node groups and the current speaking
node group.
4. Refactor this.navigationState_.currentCharIndex and
this.navigationState_.isUserSelectContent. to this.currentCharIndex_ and
this.isUserSelectContent_ to keep code concise.

Also, we added a test for the case of invalidated nodes. Test credits to
joelriley@google.com.

Bug: 1160004
Change-Id: I92e0a1acc25265df476ea248be39d05ec06726d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2587565
Commit-Queue: Lei Shi <leileilei@google.com>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#839018}
parent 6de88b2e
...@@ -128,81 +128,69 @@ class SelectToSpeak { ...@@ -128,81 +128,69 @@ class SelectToSpeak {
/** @private {boolean} */ /** @private {boolean} */
this.readAfterClose_ = true; this.readAfterClose_ = true;
/** @private {?ParagraphUtils.NodeGroupItem} */
this.currentNode_ = null;
/** @private {number} */
this.currentNodeGroupIndex_ = -1;
/** @private {?ParagraphUtils.NodeGroup} */
this.currentNodeGroup_ = null;
/**
* The indexes within the current node representing the word currently being
* spoken. Only updated if word highlighting is enabled.
* @private {?Object}
*/
this.currentNodeWord_ = null;
/**
* These navigation state variables enable us to separate the
* functionalities of node enqueueing, navigation control, and TTS playback.
* @private {!{currentNodes:!Array<AutomationNode>,
* currentNodeGroupStartNodeIndex: number,
* currentCharIndex: number,
* currentStartCharIndex: (number|undefined),
* currentEndCharIndex: (number|undefined),
* supportsNavigationPanel: boolean}}
* isUserSelectedContent: boolean}}
*/
this.navigationState_ = {
/** /**
* The current enqueued Automation nodes. * The node groups to be spoken. We process content into node groups and
* pass one node group at a time to the TTS engine. Note that we do not use
* node groups for user-selected text in Gsuite. See readNodesInSelection_.
* @private {!Array<!ParagraphUtils.NodeGroup>}
*/ */
currentNodes: [], this.currentNodeGroups_ = [];
/** /**
* The index indicating which node in |currentNodes| is used as the start * The index for the node group currently being spoken in
* of the current NodeGroup. STS will select a few nodes from * |this.currentNodeGroups_|.
* |currentNodes| to form a NodeGroup, which is the basic unit for a * @private {number}
* TTS utterance. A NodeGroup could be a paragraph and a block of text.
* This number is used to generate a NodeGroup from |currentNodes|.
*/ */
currentNodeGroupStartNodeIndex: -1, this.currentNodeGroupIndex_ = -1;
/** /**
* The start char index of the word to be spoken. The index is relative * The node group item currently being spoken. A node group item is a
* to the NodeGroup. * representation of the original input nodes, but may not be the same. For
* example, an input inline text node will be represented by its static text
* node in the node group item.
* @private {?ParagraphUtils.NodeGroupItem}
*/ */
currentCharIndex: -1, this.currentNodeGroupItem_ = null;
/** /**
* The current start offset. The index is relative to the Automation node. * The index for the current node group item within the current node group,
* When a user selects text, they could start from the middle of a node. * The current node group can be accessed from |this.currentNodeGroups_|
* This variable indicates where the user selection starts from within * using |this.currentNodeGroupIndex_|. In most cases,
* the first node of |currentNodes|. If set to undefined, we will use the * |this.currentNodeGroupItemIndex_| can be used to get
* entire first node. * |this.currentNodeGroupItem_| from the current node group. However, in
* Gsuite, we will have node group items outside of a node group.
* @private {number}
*/ */
currentStartCharIndex: undefined, this.currentNodeGroupItemIndex_ = -1;
/** /**
* The current end offset - see currentStartCharIndex, above. * The indexes within the current node group item representing the word
* currently being spoken. Only updated if word highlighting is enabled.
* @private {?Object}
*/ */
currentEndCharIndex: undefined, this.currentNodeWord_ = null;
/** /**
* Whether the current nodes support use of the navigation panel. * The start char index of the word to be spoken. The index is relative
* to the text content of the current node group.
* @private {number}
*/ */
supportsNavigationPanel: true, this.currentCharIndex_ = -1;
/** /**
* Whether we are reading user-selected content. True if the current * Whether we are reading user-selected content. True if the current
* content is from mouse or keyboard selection. False if the current * content is from mouse or keyboard selection. False if the current
* content is processed by the navigation features like paragraph * content is processed by the navigation features like paragraph
* navigation, sentence navigation, pause and resume. * navigation, sentence navigation, pause and resume.
* @private {boolean}
*/ */
isUserSelectedContent: false, this.isUserSelectedContent_ = false;
};
/**
* Whether the current nodes support use of the navigation panel.
* @private {boolean}
*/
this.supportsNavigationPanel_ = true;
/** /**
* The position of the current focus ring, which usually highlights the * The position of the current focus ring, which usually highlights the
...@@ -212,9 +200,6 @@ class SelectToSpeak { ...@@ -212,9 +200,6 @@ class SelectToSpeak {
*/ */
this.currentFocusRing_ = []; this.currentFocusRing_ = [];
/** @private {?AutomationNode} */
this.currentBlockParent_ = null;
/** @private {boolean} */ /** @private {boolean} */
this.visible_ = true; this.visible_ = true;
...@@ -282,6 +267,14 @@ class SelectToSpeak { ...@@ -282,6 +267,14 @@ class SelectToSpeak {
this.overrideSpeechRate_ = null; this.overrideSpeechRate_ = null;
} }
/**
* Gets the node group currently being spoken.
* @return {!ParagraphUtils.NodeGroup|undefined}
*/
getCurrentNodeGroup_() {
return this.currentNodeGroups_[this.currentNodeGroupIndex_];
}
/** /**
* Determines if navigation controls should be shown (and other related * Determines if navigation controls should be shown (and other related
* functionality, such as auto-dismiss and click-to-navigate to sentence, * functionality, such as auto-dismiss and click-to-navigate to sentence,
...@@ -291,7 +284,7 @@ class SelectToSpeak { ...@@ -291,7 +284,7 @@ class SelectToSpeak {
shouldShowNavigationControls_() { shouldShowNavigationControls_() {
return this.navigationControlFlag_ && return this.navigationControlFlag_ &&
this.prefsManager_.navigationControlsEnabled() && this.prefsManager_.navigationControlsEnabled() &&
this.navigationState_.supportsNavigationPanel; this.supportsNavigationPanel_;
} }
/** /**
...@@ -520,6 +513,8 @@ class SelectToSpeak { ...@@ -520,6 +513,8 @@ class SelectToSpeak {
MetricsUtils.recordStartEvent( MetricsUtils.recordStartEvent(
MetricsUtils.StartSpeechMethod.KEYSTROKE, this.prefsManager_); MetricsUtils.StartSpeechMethod.KEYSTROKE, this.prefsManager_);
} else { } else {
// Gsuite apps include webapps beyond Docs, see getGSuiteAppRoot and
// GSUITE_APP_REGEXP.
const gsuiteAppRootNode = getGSuiteAppRoot(focusedNode); const gsuiteAppRootNode = getGSuiteAppRoot(focusedNode);
if (!gsuiteAppRootNode) { if (!gsuiteAppRootNode) {
return; return;
...@@ -532,7 +527,7 @@ class SelectToSpeak { ...@@ -532,7 +527,7 @@ class SelectToSpeak {
} }
const tab = tabs[0]; const tab = tabs[0];
this.inputHandler_.onRequestReadClipboardData(); this.inputHandler_.onRequestReadClipboardData();
this.currentNode_ = this.currentNodeGroupItem_ =
new ParagraphUtils.NodeGroupItem(gsuiteAppRootNode, 0, false); new ParagraphUtils.NodeGroupItem(gsuiteAppRootNode, 0, false);
chrome.tabs.executeScript(tab.id, { chrome.tabs.executeScript(tab.id, {
allFrames: true, allFrames: true,
...@@ -648,9 +643,9 @@ class SelectToSpeak { ...@@ -648,9 +643,9 @@ class SelectToSpeak {
* |this.ttsPaused_| was true while tts is speaking. This function also sets * |this.ttsPaused_| was true while tts is speaking. This function also sets
* the |this.pauseCompleteCallback_|, which will be executed at the end of * the |this.pauseCompleteCallback_|, which will be executed at the end of
* the pause process in |updatePauseStatusFromTtsEvent_|. This enables us to * the pause process in |updatePauseStatusFromTtsEvent_|. This enables us to
* execute functions when the pause request is finished. For example, we can * execute functions when the pause request is finished. For example, to
* adjust |this.navigationState_| after a pause is fulfilled, and then resume * navigate the next sentence, we trigger pause_ and start finding the next
* to navigate to different positions. * sentence when the pause function is fulfilled.
* @return {!Promise} * @return {!Promise}
* @private * @private
*/ */
...@@ -666,7 +661,7 @@ class SelectToSpeak { ...@@ -666,7 +661,7 @@ class SelectToSpeak {
// user-selected content. This enables us to distinguish between a user- // user-selected content. This enables us to distinguish between a user-
// trigger pause from the auto pause happening at the end of user-selected // trigger pause from the auto pause happening at the end of user-selected
// content. // content.
this.navigationState_.isUserSelectedContent = false; this.isUserSelectedContent_ = false;
}); });
} }
...@@ -684,15 +679,16 @@ class SelectToSpeak { ...@@ -684,15 +679,16 @@ class SelectToSpeak {
if (!this.isPaused_()) { if (!this.isPaused_()) {
return; return;
} }
const currentNodeGroup = this.getCurrentNodeGroup_();
// If there is no processed node group, that means the user has not selected // If there is no processed node group, that means the user has not selected
// anything. Ignore the resume command. // anything. Ignore the resume command.
if (!this.currentNodeGroup_) { if (!currentNodeGroup) {
return; return;
} }
const {nodes: remainingNodes, offset} = const {nodes: remainingNodes, offset} =
NodeUtils.getNextNodesInParagraphFromNodeGroup( NodeUtils.getNextNodesInParagraphFromNodeGroup(
this.currentNodeGroup_, this.navigationState_.currentCharIndex, currentNodeGroup, this.currentCharIndex_, constants.Dir.FORWARD);
constants.Dir.FORWARD);
// There is no remaining nodes in this paragraph so we navigate to the next // There is no remaining nodes in this paragraph so we navigate to the next
// paragraph. // paragraph.
if (remainingNodes.length === 0) { if (remainingNodes.length === 0) {
...@@ -700,9 +696,9 @@ class SelectToSpeak { ...@@ -700,9 +696,9 @@ class SelectToSpeak {
return; return;
} }
if (this.navigationState_.isUserSelectedContent || if (this.isUserSelectedContent_ ||
SentenceUtils.isSentenceStart( SentenceUtils.isSentenceStart(
this.currentNodeGroup_, this.navigationState_.currentCharIndex)) { currentNodeGroup, this.currentCharIndex_)) {
// If we are resuming from the end of user-selected content or if we are // If we are resuming from the end of user-selected content or if we are
// at the start of the current sentence, we should start reading the // at the start of the current sentence, we should start reading the
// remaining content. // remaining content.
...@@ -728,27 +724,10 @@ class SelectToSpeak { ...@@ -728,27 +724,10 @@ class SelectToSpeak {
stopAll_() { stopAll_() {
chrome.tts.stop(); chrome.tts.stop();
this.clearFocusRing_(); this.clearFocusRing_();
this.clearNavigationStateVariables_();
this.overrideSpeechRate_ = null; // Reset speech rate to system default this.overrideSpeechRate_ = null; // Reset speech rate to system default
this.onStateChanged_(SelectToSpeakState.INACTIVE); this.onStateChanged_(SelectToSpeakState.INACTIVE);
} }
/**
* Clears the member variables for the navigation state.
* @private
*/
clearNavigationStateVariables_() {
this.navigationState_ = {
currentNodes: [],
currentNodeGroupStartNodeIndex: -1,
currentCharIndex: -1,
currentStartCharIndex: undefined,
currentEndCharIndex: undefined,
supportsNavigationPanel: true,
isUserSelectedContent: false,
};
}
/** /**
* Clears the current focus ring and node, but does * Clears the current focus ring and node, but does
* not stop the speech. * not stop the speech.
...@@ -757,14 +736,27 @@ class SelectToSpeak { ...@@ -757,14 +736,27 @@ class SelectToSpeak {
clearFocusRingAndNode_() { clearFocusRingAndNode_() {
this.clearFocusRing_(); this.clearFocusRing_();
// Clear the node and also stop the interval testing. // Clear the node and also stop the interval testing.
this.currentNode_ = null; this.resetNodes_();
this.currentNodeGroupIndex_ = -1; this.supportsNavigationPanel_ = true;
this.currentNodeWord_ = null; this.isUserSelectedContent_ = false;
clearInterval(this.intervalId_); clearInterval(this.intervalId_);
this.intervalId_ = undefined; this.intervalId_ = undefined;
this.scrollToSpokenNode_ = false; this.scrollToSpokenNode_ = false;
} }
/**
* Resets the instance variables for nodes and node groups.
* @private
*/
resetNodes_() {
this.currentNodeGroups_ = [];
this.currentNodeGroupIndex_ = -1;
this.currentNodeGroupItem_ = null;
this.currentNodeGroupItemIndex_ = -1;
this.currentNodeWord_ = null;
this.currentCharIndex_ = -1;
}
/** /**
* Update the navigation floating panel. * Update the navigation floating panel.
* @private * @private
...@@ -1037,9 +1029,11 @@ class SelectToSpeak { ...@@ -1037,9 +1029,11 @@ class SelectToSpeak {
* @private * @private
*/ */
async navigateToNextSentence_(direction, skipCurrentSentence = false) { async navigateToNextSentence_(direction, skipCurrentSentence = false) {
const currentNodeGroup = this.getCurrentNodeGroup_();
// An empty node group is not expected and means that the user has not // An empty node group is not expected and means that the user has not
// enqueued any text. // enqueued any text.
if (!this.currentNodeGroup_) { if (!currentNodeGroup) {
return; return;
} }
...@@ -1050,8 +1044,8 @@ class SelectToSpeak { ...@@ -1050,8 +1044,8 @@ class SelectToSpeak {
// Checks the next sentence within this node group. If we have enqueued the // Checks the next sentence within this node group. If we have enqueued the
// next sentence that fulfilled the requirements, return. // next sentence that fulfilled the requirements, return.
if (this.enqueueNextSentenceWithinNodeGroup_( if (this.enqueueNextSentenceWithinNodeGroup_(
this.currentNodeGroup_, this.navigationState_.currentCharIndex, currentNodeGroup, this.currentCharIndex_, direction,
direction, skipCurrentSentence)) { skipCurrentSentence)) {
return; return;
} }
...@@ -1063,8 +1057,7 @@ class SelectToSpeak { ...@@ -1063,8 +1057,7 @@ class SelectToSpeak {
// searching backward, the offset is the char index pointing to the char // searching backward, the offset is the char index pointing to the char
// after the remaining content. // after the remaining content.
const {nodes, offset} = NodeUtils.getNextNodesInParagraphFromNodeGroup( const {nodes, offset} = NodeUtils.getNextNodesInParagraphFromNodeGroup(
this.currentNodeGroup_, this.navigationState_.currentCharIndex, currentNodeGroup, this.currentCharIndex_, direction);
direction);
// If we have reached to the end of a paragraph, enqueue the sentence from // If we have reached to the end of a paragraph, enqueue the sentence from
// the next paragraph. // the next paragraph.
if (nodes.length === 0) { if (nodes.length === 0) {
...@@ -1090,12 +1083,12 @@ class SelectToSpeak { ...@@ -1090,12 +1083,12 @@ class SelectToSpeak {
} }
// When searching backward, we need to adjust |skipCurrentSentence| if it // When searching backward, we need to adjust |skipCurrentSentence| if it
// is true. The remaining content we get excludes the char at // is true. The remaining content we get excludes the char at
// |this.navigationState_.currentCharIndex|. If this char is a sentence // |this.currentCharIndex_|. If this char is a sentence
// start, we have already skipped the current sentence so we need to change // start, we have already skipped the current sentence so we need to change
// |skipCurrentSentence| to false for the next search. // |skipCurrentSentence| to false for the next search.
if (direction === constants.Dir.BACKWARD && skipCurrentSentence) { if (direction === constants.Dir.BACKWARD && skipCurrentSentence) {
const currentPositionIsSentenceStart = SentenceUtils.isSentenceStart( const currentPositionIsSentenceStart = SentenceUtils.isSentenceStart(
this.currentNodeGroup_, this.navigationState_.currentCharIndex); currentNodeGroup, this.currentCharIndex_);
if (currentPositionIsSentenceStart) { if (currentPositionIsSentenceStart) {
skipCurrentSentence = false; skipCurrentSentence = false;
} }
...@@ -1264,12 +1257,16 @@ class SelectToSpeak { ...@@ -1264,12 +1257,16 @@ class SelectToSpeak {
locateNodesForNextParagraph_(direction) { locateNodesForNextParagraph_(direction) {
// Use current block parent as starting point to navigate from. If it is not // Use current block parent as starting point to navigate from. If it is not
// a valid block, then use one of the nodes that are currently activated. // a valid block, then use one of the nodes that are currently activated.
let node = this.currentBlockParent_; const currentNodeGroup = this.getCurrentNodeGroup_();
if ((node === null || node.isRootNode) && this.currentNodeGroup_ && if (!currentNodeGroup) {
this.currentNodeGroup_.nodes.length > 0) { return [];
node = this.currentNodeGroup_.nodes[0].node;
} }
if (node === null) { let node = currentNodeGroup.blockParent;
if ((node === null || node.isRootNode || node.role === undefined) &&
currentNodeGroup.nodes.length > 0) {
node = currentNodeGroup.nodes[0].node;
}
if (node === null || node.role === undefined) {
// Could not find any nodes to navigate from. // Could not find any nodes to navigate from.
return []; return [];
} }
...@@ -1333,8 +1330,7 @@ class SelectToSpeak { ...@@ -1333,8 +1330,7 @@ class SelectToSpeak {
/** /**
* Enqueue nodes to TTS queue and start TTS. This function can be used for * Enqueue nodes to TTS queue and start TTS. This function can be used for
* adding nodes, either from user selection (e.g., mouse selection) or * adding nodes, either from user selection (e.g., mouse selection) or
* navigation control (e.g., next paragraph). This function will overwrite * navigation control (e.g., next paragraph).
* |this.navigationState_| and start TTS according to the offsets.
* @param {!Array<AutomationNode>} nodes The nodes to speak. * @param {!Array<AutomationNode>} nodes The nodes to speak.
* @param {!{clearFocusRing: (boolean|undefined), * @param {!{clearFocusRing: (boolean|undefined),
* startCharIndex: (number|undefined), * startCharIndex: (number|undefined),
...@@ -1357,7 +1353,7 @@ class SelectToSpeak { ...@@ -1357,7 +1353,7 @@ class SelectToSpeak {
const clearFocusRing = params.clearFocusRing || false; const clearFocusRing = params.clearFocusRing || false;
let startCharIndex = params.startCharIndex; let startCharIndex = params.startCharIndex;
let endCharIndex = params.endCharIndex; let endCharIndex = params.endCharIndex;
const isUserSelectedContent = params.isUserSelectedContent || false; this.isUserSelectedContent_ = params.isUserSelectedContent || false;
this.prepareForSpeech_(clearFocusRing /* clear the focus ring */); this.prepareForSpeech_(clearFocusRing /* clear the focus ring */);
...@@ -1380,101 +1376,96 @@ class SelectToSpeak { ...@@ -1380,101 +1376,96 @@ class SelectToSpeak {
endCharIndex = undefined; endCharIndex = undefined;
} }
// Update the navigation state variables. this.supportsNavigationPanel_ = this.isNavigationPanelSupported_(nodes);
this.navigationState_ = { this.updateNodeGroups_(nodes, startCharIndex, endCharIndex);
currentNodes: nodes,
currentNodeGroupStartNodeIndex: 0,
currentCharIndex: 0,
currentStartCharIndex: startCharIndex,
currentEndCharIndex: endCharIndex,
supportsNavigationPanel: this.isNavigationPanelSupported_(nodes),
isUserSelectedContent,
};
// Play TTS according to the current state variables. // Play TTS according to the current state variables.
this.startCurrentNodeGroup_(); this.startCurrentNodeGroup_();
} }
/** /**
* Start TTS according to the five variables in |this.navigationState_|. This * Updates the node groups to be spoken. Converts |nodes|, |startCharIndex|,
* function will first construct a NodeGroup based on |currentNodes| and * and |endCharIndex| into node groups, and updates |this.currentNodeGroups_|
* |currentNodeGroupStartNodeIndex|. Then, it will clip texts based on * and |this.currentNodeGroupIndex_|.
* |currentCharIndex|, |currentStartCharIndex|, and |currentEndCharIndex|. * @param {!Array<AutomationNode>} nodes The nodes to speak.
* Lastly, it will start TTS using the processed text. * @param {number=} startCharIndex The index into the first node's text at
* which to start speaking. If this is not passed, will start at 0.
* @param {number=} endCharIndex The index into the last node's text at which
* to end speech. If this is not passed, will stop at the end.
* @private * @private
*/ */
startCurrentNodeGroup_() { updateNodeGroups_(nodes, startCharIndex, endCharIndex) {
const currentNodes = this.navigationState_.currentNodes; this.resetNodes_();
const currentNodeGroupStartNodeIndex =
this.navigationState_.currentNodeGroupStartNodeIndex; for (let i = 0; i < nodes.length; i++) {
const currentCharIndex = this.navigationState_.currentCharIndex; // When navigation controls are enabled, disable the clipping of overflow
const currentStartCharIndex = this.navigationState_.currentStartCharIndex; // words. When overflow words are clipped, words scrolled out of view are
const currentEndCharIndex = this.navigationState_.currentEndCharIndex; // clipped, which is undesirable for our navigation features as we
// generate node groups for next/previous paragraphs which may be fully or
// Reaches to the end of the current nodes. // partially scrolled out of view.
if (currentNodeGroupStartNodeIndex >= currentNodes.length) { const nodeGroup = ParagraphUtils.buildNodeGroup(nodes, i, {
return;
}
// When navigation controls are enabled, disable the
// clipping of overflow words. When overflow words are clipped, words
// scrolled out of view are clipped, which is undesirable for our navigation
// features as we generate node groups for next/previous paragraphs which
// may be fully or partially scrolled out of view.
const nodeGroup = ParagraphUtils.buildNodeGroup(
currentNodes, currentNodeGroupStartNodeIndex, {
splitOnLanguage: this.enableLanguageDetectionIntegration_, splitOnLanguage: this.enableLanguageDetectionIntegration_,
clipOverflowWords: !this.shouldShowNavigationControls_(), clipOverflowWords: !this.shouldShowNavigationControls_(),
}); });
// |currentCharIndex| is the start char index of the word to be spoken in const isFirstNodeGroup = i === 0;
// the nodeGroup text. If the |currentCharIndex| is non-zero, that means we
// are resuming from prior TTS. We trim the text regardless which NodeGroup
// we are, as a user could possibly pause at any NodeGroup.
this.applyOffset(nodeGroup, currentCharIndex, true /* isStartOffset */);
const isFirstNodeGroup = currentNodeGroupStartNodeIndex === 0;
const shouldApplyStartOffset = const shouldApplyStartOffset =
isFirstNodeGroup && currentStartCharIndex !== undefined; isFirstNodeGroup && startCharIndex !== undefined;
const firstNodeHasInlineText = const firstNodeHasInlineText =
nodeGroup.nodes.length > 0 && nodeGroup.nodes[0].hasInlineText; nodeGroup.nodes.length > 0 && nodeGroup.nodes[0].hasInlineText;
if (shouldApplyStartOffset && firstNodeHasInlineText) { if (shouldApplyStartOffset && firstNodeHasInlineText) {
// We assume that the start offset will only be applied to the first node // We assume that the start offset will only be applied to the first
// in the first NodeGroup. The |currentStartCharIndex| needs to be // node in the first NodeGroup. The |startCharIndex| needs to be
// adjusted. The first node of the NodeGroup may not be at the beginning // adjusted. The first node of the NodeGroup may not be at the beginning
// of the parent of the NodeGroup. (e.g., an inlineText in its staticText // of the parent of the NodeGroup. (e.g., an inlineText in its
// parent). Thus, we need to adjust the start index. // staticText parent). Thus, we need to adjust the start index.
const startIndexInNodeParent = const startIndexInNodeParent =
ParagraphUtils.getStartCharIndexInParent(currentNodes[0]); ParagraphUtils.getStartCharIndexInParent(nodes[0]);
const startIndexInNodeGroup = currentStartCharIndex + const startIndexInNodeGroup = startCharIndex + startIndexInNodeParent +
startIndexInNodeParent + nodeGroup.nodes[0].startChar; nodeGroup.nodes[0].startChar;
this.applyOffset( this.applyOffset(
nodeGroup, startIndexInNodeGroup, true /* isStartOffset */); nodeGroup, startIndexInNodeGroup, true /* isStartOffset */);
} }
const isLastNodeGroup = (nodeGroup.endIndex === currentNodes.length - 1); // Advance i to the end of this group, to skip all nodes it contains.
i = nodeGroup.endIndex;
const isLastNodeGroup = (i === nodes.length - 1);
const shouldApplyEndOffset = const shouldApplyEndOffset =
isLastNodeGroup && currentEndCharIndex !== undefined; isLastNodeGroup && endCharIndex !== undefined;
const lastNodeHasInlineText = nodeGroup.nodes.length > 0 && const lastNodeHasInlineText = nodeGroup.nodes.length > 0 &&
nodeGroup.nodes[nodeGroup.nodes.length - 1].hasInlineText; nodeGroup.nodes[nodeGroup.nodes.length - 1].hasInlineText;
if (shouldApplyEndOffset && lastNodeHasInlineText) { if (shouldApplyEndOffset && lastNodeHasInlineText) {
// We assume that the end offset will only be applied to the last node in // We assume that the end offset will only be applied to the last node
// the last NodeGroup. Similarly, |currentEndCharIndex| needs to be // in the last NodeGroup. Similarly, |endCharIndex| needs to be
// adjusted. // adjusted.
const startIndexInNodeParent = ParagraphUtils.getStartCharIndexInParent( const startIndexInNodeParent =
currentNodes[nodeGroup.endIndex]); ParagraphUtils.getStartCharIndexInParent(nodes[i]);
const endIndexInNodeGroup = currentEndCharIndex + startIndexInNodeParent + const endIndexInNodeGroup = endCharIndex + startIndexInNodeParent +
nodeGroup.nodes[nodeGroup.nodes.length - 1].startChar; nodeGroup.nodes[nodeGroup.nodes.length - 1].startChar;
this.applyOffset( this.applyOffset(
nodeGroup, endIndexInNodeGroup, false /* isStartOffset */); nodeGroup, endIndexInNodeGroup, false /* isStartOffset */);
} }
if (nodeGroup.nodes.length === 0 && !isLastNodeGroup) { if (nodeGroup.nodes.length === 0 && !isLastNodeGroup) {
// If the current nodeGroup is empty, we end this node group early, as if continue;
// we have completed this node group. }
this.onNodeGroupSpeakingCompleted_( this.currentNodeGroups_.push(nodeGroup);
currentNodeGroupStartNodeIndex /* currentNodeGroupEndIndex */); }
// Sets the initial node group index to zero if this.currentNodeGroups_ has
// items.
if (this.currentNodeGroups_.length > 0) {
this.currentNodeGroupIndex_ = 0;
}
} }
/**
* Starts reading the current node group.
* @private
*/
startCurrentNodeGroup_() {
const nodeGroup = this.getCurrentNodeGroup_();
if (!nodeGroup) {
return;
}
const options = {}; const options = {};
// Copy options so we can add lang below // Copy options so we can add lang below
Object.assign(options, this.prefsManager_.speechOptions()); Object.assign(options, this.prefsManager_.speechOptions());
...@@ -1491,30 +1482,24 @@ class SelectToSpeak { ...@@ -1491,30 +1482,24 @@ class SelectToSpeak {
options.onEvent = (event) => { options.onEvent = (event) => {
if (event.type === 'start' && nodeGroup.nodes.length > 0) { if (event.type === 'start' && nodeGroup.nodes.length > 0) {
this.updatePauseStatusFromTtsEvent_(false /* shouldPause */); this.updatePauseStatusFromTtsEvent_(false /* shouldPause */);
// TODO(leileilei): We can get rid of this.currentBlockParent_ if it is
// always equal to currentNodeGroup_.blockParent.
this.currentBlockParent_ = nodeGroup.blockParent;
this.currentNodeGroup_ = nodeGroup;
// Update |currentCharIndex|. Find the first non-space char index in // Update |this.currentCharIndex_|. Find the first non-space char index
// nodeGroup text, or 0 if the text is undefined or the first char is // in nodeGroup text, or 0 if the text is undefined or the first char is
// non-space. // non-space.
this.navigationState_.currentCharIndex = nodeGroupText.search(/\S|$/); this.currentCharIndex_ = nodeGroupText.search(/\S|$/);
this.syncCurrentNodeWithCharIndex_( this.syncCurrentNodeWithCharIndex_(nodeGroup, this.currentCharIndex_);
nodeGroup, this.navigationState_.currentCharIndex /* charIndex */);
if (this.prefsManager_.wordHighlightingEnabled()) { if (this.prefsManager_.wordHighlightingEnabled()) {
// At 'start', find the first word and highlight that. Clear the // At 'start', find the first word and highlight that. Clear the
// previous word in the node. // previous word in the node.
this.currentNodeWord_ = null; this.currentNodeWord_ = null;
// If |currentCharIndex| is not 0, that means we have applied a start // If |this.currentCharIndex_| is not 0, that means we have applied a
// offset. Thus, we need to pass startIndexInNodeGroup to // start offset. Thus, we need to pass startIndexInNodeGroup to
// opt_startIndex and overwrite the word boundaries in the original // opt_startIndex and overwrite the word boundaries in the original
// node. // node.
this.updateNodeHighlight_( this.updateNodeHighlight_(
nodeGroupText, this.navigationState_.currentCharIndex, nodeGroupText, this.currentCharIndex_,
this.navigationState_.currentCharIndex !== 0 ? this.currentCharIndex_ !== 0 ? this.currentCharIndex_ :
this.navigationState_.currentCharIndex :
undefined); undefined);
} else { } else {
this.testCurrentNode_(); this.testCurrentNode_();
...@@ -1540,9 +1525,13 @@ class SelectToSpeak { ...@@ -1540,9 +1525,13 @@ class SelectToSpeak {
this.updatePauseStatusFromTtsEvent_(true /* shouldPause */); this.updatePauseStatusFromTtsEvent_(true /* shouldPause */);
} }
} else if (event.type === 'end') { } else if (event.type === 'end') {
this.onNodeGroupSpeakingCompleted_( this.onNodeGroupSpeakingCompleted_();
nodeGroup.endIndex /* currentNodeGroupEndIndex */);
} else if (event.type === 'word') { } else if (event.type === 'word') {
// The Closure compiler doesn't realize that we did a !nodeGroup earlier
// so we check again here.
if (!nodeGroup) {
return;
}
this.onTtsWordEvent_(event, nodeGroup); this.onTtsWordEvent_(event, nodeGroup);
} }
}; };
...@@ -1554,20 +1543,18 @@ class SelectToSpeak { ...@@ -1554,20 +1543,18 @@ class SelectToSpeak {
* indicated by the end index. If we have reached the last node group, this * indicated by the end index. If we have reached the last node group, this
* function will update STS status depending whether the navigation feature is * function will update STS status depending whether the navigation feature is
* enabled. * enabled.
* @param {number} currentNodeGroupEndIndex the index of the last node in the * @private
* current node group. The index is relative to
* |this.navigationState_.currentNodes|.
*/ */
onNodeGroupSpeakingCompleted_(currentNodeGroupEndIndex) { onNodeGroupSpeakingCompleted_() {
const currentNodeGroup = this.getCurrentNodeGroup_();
// Update the current char index to the end of the text content in this // Update the current char index to the end of the text content in this
// nodeGroup. // nodeGroup.
const nodeGroupText = const nodeGroupText = (currentNodeGroup && currentNodeGroup.text) || '';
(this.currentNodeGroup_ && this.currentNodeGroup_.text) || ''; this.currentCharIndex_ = nodeGroupText.trimEnd().length;
this.navigationState_.currentCharIndex = nodeGroupText.trimEnd().length;
const isLastNodeGroup = const isLastNodeGroup =
(currentNodeGroupEndIndex === (this.currentNodeGroupIndex_ === this.currentNodeGroups_.length - 1);
this.navigationState_.currentNodes.length - 1);
if (isLastNodeGroup) { if (isLastNodeGroup) {
if (!this.shouldShowNavigationControls_()) { if (!this.shouldShowNavigationControls_()) {
this.onStateChanged_(SelectToSpeakState.INACTIVE); this.onStateChanged_(SelectToSpeakState.INACTIVE);
...@@ -1578,18 +1565,14 @@ class SelectToSpeak { ...@@ -1578,18 +1565,14 @@ class SelectToSpeak {
return; return;
} }
// Navigate to the next NodeGroup. Don't change |currentNodes|, // Start reading the next node group.
// |currentStartCharIndex|, or |currentEndCharIndex|. this.currentNodeGroupIndex_++;
this.navigationState_.currentCharIndex = 0;
this.navigationState_.currentNodeGroupStartNodeIndex =
currentNodeGroupEndIndex + 1;
// Play TTS.
this.startCurrentNodeGroup_(); this.startCurrentNodeGroup_();
} }
/** /**
* Update |this.currentNode_|, the current speaking or the node to be spoken * Update |this.currentNodeGroupItem_|, the current speaking or the node to be
* in the node group. * spoken in the node group.
* @param {ParagraphUtils.NodeGroup} nodeGroup the current nodeGroup. * @param {ParagraphUtils.NodeGroup} nodeGroup the current nodeGroup.
* @param {number} charIndex the start char index of the word to be spoken. * @param {number} charIndex the start char index of the word to be spoken.
* The index is relative to the entire NodeGroup. * The index is relative to the entire NodeGroup.
...@@ -1604,19 +1587,22 @@ class SelectToSpeak { ...@@ -1604,19 +1587,22 @@ class SelectToSpeak {
opt_startFromNodeGroupIndex = 0; opt_startFromNodeGroupIndex = 0;
} }
// There is no speaking word, set the NodeGroupIndex to 0. // There is no speaking word, set the NodeGroupItemIndex to 0.
if (charIndex <= 0) { if (charIndex <= 0) {
this.currentNodeGroupIndex_ = 0; this.currentNodeGroupItemIndex_ = 0;
this.currentNode_ = nodeGroup.nodes[this.currentNodeGroupIndex_]; this.currentNodeGroupItem_ =
return this.currentNodeGroupIndex_ === opt_startFromNodeGroupIndex; nodeGroup.nodes[this.currentNodeGroupItemIndex_];
return this.currentNodeGroupItemIndex_ === opt_startFromNodeGroupIndex;
} }
// Sets the this.currentNodeGroupIndex_ to |opt_startFromNodeGroupIndex| // Sets the |this.currentNodeGroupItemIndex_| to
this.currentNodeGroupIndex_ = opt_startFromNodeGroupIndex; // |opt_startFromNodeGroupIndex|
this.currentNode_ = nodeGroup.nodes[this.currentNodeGroupIndex_]; this.currentNodeGroupItemIndex_ = opt_startFromNodeGroupIndex;
this.currentNodeGroupItem_ =
nodeGroup.nodes[this.currentNodeGroupItemIndex_];
if (this.currentNodeGroupIndex_ + 1 < nodeGroup.nodes.length) { if (this.currentNodeGroupItemIndex_ + 1 < nodeGroup.nodes.length) {
let next = nodeGroup.nodes[this.currentNodeGroupIndex_ + 1]; let next = nodeGroup.nodes[this.currentNodeGroupItemIndex_ + 1];
let nodeUpdated = false; let nodeUpdated = false;
// TODO(katie): For something like a date, the start and end // TODO(katie): For something like a date, the start and end
// node group nodes can actually be different. Example: // node group nodes can actually be different. Example:
...@@ -1626,7 +1612,7 @@ class SelectToSpeak { ...@@ -1626,7 +1612,7 @@ class SelectToSpeak {
// start char index of the target word, we just need to make sure the // start char index of the target word, we just need to make sure the
// next.startchar is bigger than it. // next.startchar is bigger than it.
while (next && charIndex >= next.startChar && while (next && charIndex >= next.startChar &&
this.currentNodeGroupIndex_ + 1 < nodeGroup.nodes.length) { this.currentNodeGroupItemIndex_ + 1 < nodeGroup.nodes.length) {
next = this.incrementCurrentNodeAndGetNext_(nodeGroup); next = this.incrementCurrentNodeAndGetNext_(nodeGroup);
nodeUpdated = true; nodeUpdated = true;
} }
...@@ -1681,10 +1667,9 @@ class SelectToSpeak { ...@@ -1681,10 +1667,9 @@ class SelectToSpeak {
onTtsWordEvent_(event, nodeGroup) { onTtsWordEvent_(event, nodeGroup) {
// Not all speech engines include length in the ttsEvent object. . // Not all speech engines include length in the ttsEvent object. .
const hasLength = event.length !== undefined && event.length >= 0; const hasLength = event.length !== undefined && event.length >= 0;
// Only update the currentCharIndex if event has a higher charIndex. TTS // Only update the |this.currentCharIndex_| if event has a higher charIndex.
// sometimes will report an incorrect number at the end of an utterance. // TTS sometimes will report an incorrect number at the end of an utterance.
this.navigationState_.currentCharIndex = this.currentCharIndex_ = Math.max(event.charIndex, this.currentCharIndex_);
Math.max(event.charIndex, this.navigationState_.currentCharIndex);
console.debug(nodeGroup.text + ' (index ' + event.charIndex + ')'); console.debug(nodeGroup.text + ' (index ' + event.charIndex + ')');
let debug = '-'.repeat(event.charIndex); let debug = '-'.repeat(event.charIndex);
if (hasLength) { if (hasLength) {
...@@ -1695,10 +1680,10 @@ class SelectToSpeak { ...@@ -1695,10 +1680,10 @@ class SelectToSpeak {
console.debug(debug); console.debug(debug);
// First determine which node contains the word currently being spoken, // First determine which node contains the word currently being spoken,
// and update this.currentNode_, this.currentNodeWord_, and // and update this.currentNodeGroupItem_, this.currentNodeWord_, and
// this.currentNodeGroupIndex_ to match. // this.currentNodeGroupItemIndex_ to match.
const nodeUpdated = this.syncCurrentNodeWithCharIndex_( const nodeUpdated = this.syncCurrentNodeWithCharIndex_(
nodeGroup, event.charIndex, this.currentNodeGroupIndex_); nodeGroup, event.charIndex, this.currentNodeGroupItemIndex_);
if (nodeUpdated) { if (nodeUpdated) {
if (!this.prefsManager_.wordHighlightingEnabled()) { if (!this.prefsManager_.wordHighlightingEnabled()) {
// If we are doing a per-word highlight, we will test the // If we are doing a per-word highlight, we will test the
...@@ -1712,8 +1697,9 @@ class SelectToSpeak { ...@@ -1712,8 +1697,9 @@ class SelectToSpeak {
if (this.prefsManager_.wordHighlightingEnabled()) { if (this.prefsManager_.wordHighlightingEnabled()) {
if (hasLength) { if (hasLength) {
this.currentNodeWord_ = { this.currentNodeWord_ = {
'start': event.charIndex - this.currentNode_.startChar, 'start': event.charIndex - this.currentNodeGroupItem_.startChar,
'end': event.charIndex + event.length - this.currentNode_.startChar 'end': event.charIndex + event.length -
this.currentNodeGroupItem_.startChar
}; };
this.testCurrentNode_(); this.testCurrentNode_();
} else { } else {
...@@ -1736,15 +1722,16 @@ class SelectToSpeak { ...@@ -1736,15 +1722,16 @@ class SelectToSpeak {
*/ */
incrementCurrentNodeAndGetNext_(nodeGroup) { incrementCurrentNodeAndGetNext_(nodeGroup) {
// Move to the next node. // Move to the next node.
this.currentNodeGroupIndex_ += 1; this.currentNodeGroupItemIndex_ += 1;
this.currentNode_ = nodeGroup.nodes[this.currentNodeGroupIndex_]; this.currentNodeGroupItem_ =
nodeGroup.nodes[this.currentNodeGroupItemIndex_];
// Setting this.currentNodeWord_ to null signals it should be recalculated // Setting this.currentNodeWord_ to null signals it should be recalculated
// later. // later.
this.currentNodeWord_ = null; this.currentNodeWord_ = null;
if (this.currentNodeGroupIndex_ + 1 >= nodeGroup.nodes.length) { if (this.currentNodeGroupItemIndex_ + 1 >= nodeGroup.nodes.length) {
return null; return null;
} }
return nodeGroup.nodes[this.currentNodeGroupIndex_ + 1]; return nodeGroup.nodes[this.currentNodeGroupItemIndex_ + 1];
} }
/** /**
...@@ -1877,9 +1864,13 @@ class SelectToSpeak { ...@@ -1877,9 +1864,13 @@ class SelectToSpeak {
// TODO: Better test: has no siblings in the group, highlight just // TODO: Better test: has no siblings in the group, highlight just
// the one node. if it has siblings, highlight the parent. // the one node. if it has siblings, highlight the parent.
let focusRingRect; let focusRingRect;
if (this.currentBlockParent_ !== null && const currentNodeGroup = this.getCurrentNodeGroup_();
node.role === RoleType.INLINE_TEXT_BOX) { if (!currentNodeGroup) {
focusRingRect = this.currentBlockParent_.location; return;
}
const currentBlockParent = currentNodeGroup.blockParent;
if (currentBlockParent !== null && node.role === RoleType.INLINE_TEXT_BOX) {
focusRingRect = currentBlockParent.location;
} else { } else {
focusRingRect = node.location; focusRingRect = node.location;
} }
...@@ -1892,22 +1883,22 @@ class SelectToSpeak { ...@@ -1892,22 +1883,22 @@ class SelectToSpeak {
* @private * @private
*/ */
testCurrentNode_() { testCurrentNode_() {
if (this.currentNode_ == null) { if (this.currentNodeGroupItem_ == null) {
return; return;
} }
if (this.currentNode_.node.location === undefined) { if (this.currentNodeGroupItem_.node.location === undefined) {
// Don't do the hit test because there is no location to test against. // Don't do the hit test because there is no location to test against.
// Just directly update Select To Speak from node state. // Just directly update Select To Speak from node state.
this.updateFromNodeState_(this.currentNode_, false); this.updateFromNodeState_(this.currentNodeGroupItem_, false);
} else { } else {
this.updateHighlightAndFocus_(this.currentNode_); this.updateHighlightAndFocus_(this.currentNodeGroupItem_);
// Do a hit test to make sure the node is not in a background window // Do a hit test to make sure the node is not in a background window
// or minimimized. On the result checkCurrentNodeMatchesHitTest_ will be // or minimimized. On the result checkCurrentNodeMatchesHitTest_ will be
// called, and we will use that result plus the currentNode's state to // called, and we will use that result plus the currentNode's state to
// determine how to set the focus and whether to stop speech. // determine how to set the focus and whether to stop speech.
this.desktop_.hitTest( this.desktop_.hitTest(
this.currentNode_.node.location.left, this.currentNodeGroupItem_.node.location.left,
this.currentNode_.node.location.top, EventType.HOVER); this.currentNodeGroupItem_.node.location.top, EventType.HOVER);
} }
} }
...@@ -1917,13 +1908,13 @@ class SelectToSpeak { ...@@ -1917,13 +1908,13 @@ class SelectToSpeak {
* @private * @private
*/ */
onHitTestCheckCurrentNodeMatches_(evt) { onHitTestCheckCurrentNodeMatches_(evt) {
if (this.currentNode_ == null) { if (this.currentNodeGroupItem_ == null) {
return; return;
} }
chrome.automation.getFocus(function(focusedNode) { chrome.automation.getFocus(function(focusedNode) {
var window = NodeUtils.getNearestContainingWindow(evt.target); var window = NodeUtils.getNearestContainingWindow(evt.target);
var currentWindow = var currentWindow =
NodeUtils.getNearestContainingWindow(this.currentNode_.node); NodeUtils.getNearestContainingWindow(this.currentNodeGroupItem_.node);
var inForeground = var inForeground =
currentWindow != null && window != null && currentWindow === window; currentWindow != null && window != null && currentWindow === window;
if (!inForeground && if (!inForeground &&
...@@ -1943,7 +1934,7 @@ class SelectToSpeak { ...@@ -1943,7 +1934,7 @@ class SelectToSpeak {
NodeUtils.getNearestContainingWindow(focusedNode.root); NodeUtils.getNearestContainingWindow(focusedNode.root);
inForeground = focusedWindow != null && currentWindow === focusedWindow; inForeground = focusedWindow != null && currentWindow === focusedWindow;
} }
this.updateFromNodeState_(this.currentNode_, inForeground); this.updateFromNodeState_(this.currentNodeGroupItem_, inForeground);
}.bind(this)); }.bind(this));
} }
...@@ -1985,20 +1976,20 @@ class SelectToSpeak { ...@@ -1985,20 +1976,20 @@ class SelectToSpeak {
} }
// Get the next word based on the event's charIndex. // Get the next word based on the event's charIndex.
const nextWordStart = const nextWordStart =
WordUtils.getNextWordStart(text, charIndex, this.currentNode_); WordUtils.getNextWordStart(text, charIndex, this.currentNodeGroupItem_);
// The |WordUtils.getNextWordEnd| will find the correct end based on the // The |WordUtils.getNextWordEnd| will find the correct end based on the
// trimmed text, so there is no need to provide additional input like // trimmed text, so there is no need to provide additional input like
// opt_startIndex. // opt_startIndex.
const nextWordEnd = WordUtils.getNextWordEnd( const nextWordEnd = WordUtils.getNextWordEnd(
text, opt_startIndex === undefined ? nextWordStart : opt_startIndex, text, opt_startIndex === undefined ? nextWordStart : opt_startIndex,
this.currentNode_); this.currentNodeGroupItem_);
// Map the next word into the node's index from the text. // Map the next word into the node's index from the text.
const nodeStart = opt_startIndex === undefined ? const nodeStart = opt_startIndex === undefined ?
nextWordStart - this.currentNode_.startChar : nextWordStart - this.currentNodeGroupItem_.startChar :
opt_startIndex - this.currentNode_.startChar; opt_startIndex - this.currentNodeGroupItem_.startChar;
const nodeEnd = Math.min( const nodeEnd = Math.min(
nextWordEnd - this.currentNode_.startChar, nextWordEnd - this.currentNodeGroupItem_.startChar,
NodeUtils.nameLength(this.currentNode_.node)); NodeUtils.nameLength(this.currentNodeGroupItem_.node));
if ((this.currentNodeWord_ == null || if ((this.currentNodeWord_ == null ||
nodeStart >= this.currentNodeWord_.end) && nodeStart >= this.currentNodeWord_.end) &&
nodeStart <= nodeEnd) { nodeStart <= nodeEnd) {
......
...@@ -588,3 +588,53 @@ TEST_F( ...@@ -588,3 +588,53 @@ TEST_F(
this.mockTts.pendingUtterances()[0], '. Sentence two.'); this.mockTts.pendingUtterances()[0], '. Sentence two.');
}); });
}); });
TEST_F('SelectToSpeakNavigationControlTest', 'ResizeWhilePlaying', function() {
const longLine =
'Second paragraph is longer than 300 pixels and will wrap when resized';
const bodyHtml = `
<script type="text/javascript">
function doResize() {
document.getElementById('resize').style.width = '100px';
}
</script>
<div id="content">
<p>First paragraph</p>
<p id='resize' style='width:300px; font-size: 1em'>
${longLine}
</p>
</div>
<button onclick="doResize()">Resize</button>
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('content', bodyHtml), (root) => {
this.triggerReadSelectedText();
// Speaks the first paragraph.
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'First paragraph');
const resizeButton =
root.find({role: 'button', attributes: {name: 'Resize'}});
// Wait for click event, at which point the automation tree should
// be updated from the resize.
resizeButton.addEventListener(
EventType.CLICKED, this.newCallback(() => {
// Trigger next node group by completing first TTS request.
this.mockTts.finishPendingUtterance();
// Should still read second paragraph, even though some nodes
// were invalided from the resize.
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], longLine);
}));
// Perform resize.
resizeButton.doDefault();
});
});
\ No newline at end of file
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