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

Navigate sentence across boundaries in Select to Speak

When doing sentence navigation, we will first search for next sentence
start within the current node group. If that fails, we will look for the
rest of the content within this paragraph. If we still cannot find a
sentence start, we will look for the next paragraph.

Once we find a sentence start, we will get the nodes from the sentence
start until the end of that paragraph. Then, we will send the new
content to TTS.

AX-Relnotes: N/A.
Bug: 1143823
Change-Id: I051c572d4a9f866aeb37fba32137ed81b9cac744
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2587799
Commit-Queue: Lei Shi <leileilei@google.com>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Reviewed-by: default avatarAkihiro Ota <akihiroota@chromium.org>
Cr-Commit-Position: refs/heads/master@{#838258}
parent 806e4478
......@@ -548,32 +548,80 @@ class NodeUtils {
/**
* Gets the remaining content of a paragraph with an assigned position. The
* position is defined by the |charIndex| to the text of |nodeGroup|. The
* remaining content is returned as a list of nodes with offset.
* @param {!ParagraphUtils.NodeGroup} nodeGroup
* position is defined by the |charIndex| to the text of |nodeGroup|. If
* |direction| is set to forward, we will look for trailing content after the
* position. Otherwise, we will get the leading content before the position.
* The remaining content is returned as a list of nodes with offset.
* @param {!ParagraphUtils.NodeGroup} nodeGroup The nodeGroup of the assigned
* position. The nodeGroup may contain the content of the entire paragraph
* or only a part of the paragraph.
* @param {number} charIndex The char index of the position. The index is
* relative to the text content of the |nodeGroup|. This index is
* inclusive: if we set |charIndex| to 0, we will get the remaining
* content of the paragraph including all the content in the input
* |nodeGroup|.
* inclusive for forward searching: if we set |direction| to forward with
* a 0 |charIndex|, we will get the remaining content of the paragraph
* including all the content in the input |nodeGroup|. However, it is
* exclusive for backward searching: when searching backward with a 0
* |charIndex|, we will exclude all the content in the input |nodeGroup|.
* @param {constants.Dir} direction
* @return {!{nodes: !Array<!AutomationNode>,
* offset: number}}
* nodes: the nodes that have the remaining content.
* offset: the offset for the nodes. The offset is into the name of the
* first node, and marks the start index of the remaining content in the first
* node, inclusively. For example, "Hello" with an offset 3 indicates that the
* remaining content is "lo".
* offset: the offset for the nodes. When searching forward, the offset is
* into the name of the first node, and marks the start index of the remaining
* content in the first node, inclusively. For example, "Hello" with an offset
* 3 indicates that the remaining content is "lo". Otherwise, the offset is
* into the name of the last node, and marks the end index of the remaining
* content in the last node, exclusively. For example, "Hello" with an offset
* 3 indicates that the remaining content is "Hel".
*/
static getNextNodesInParagraphFromNodeGroupWithOffset(nodeGroup, charIndex) {
static getNextNodesInParagraphFromNodeGroup(nodeGroup, charIndex, direction) {
if (nodeGroup.nodes.length === 0) {
return {nodes: [], offset: -1};
}
let {node: startNode, offset} =
ParagraphUtils.findNodeFromNodeGroupByCharIndex(nodeGroup, charIndex);
// If we did not find a node for the charIndex, we use the last node of the
// current node group as the starting node and set offset to the length of
// the node's name. This enables us to get all the content within this
// paragraph but after the current node group.
if (direction === constants.Dir.BACKWARD) {
// If we did not find a node for the charIndex, we use the first node of
// the current node group and set offset to 0. This enables us to get all
// the content before the current node group in this paragraph.
if (!startNode) {
const firstNode = nodeGroup.nodes[0].node;
const firstChildOfFirstNode = NodeUtils.getFirstLeafChild(firstNode);
startNode = firstChildOfFirstNode || firstNode;
offset = 0;
}
// Gets all the nodes before the startNode. We include the start node
// if it is not empty. Note that this is based on the assumption that
// this function will still include nodes with overflow text.
const leadingNodes =
NodeUtils.getNextNodesInParagraph(startNode, constants.Dir.BACKWARD);
const textInStartNode =
ParagraphUtils.getNodeName(startNode).substr(0, offset);
if (!ParagraphUtils.isWhitespace(textInStartNode)) {
leadingNodes.push(startNode);
return {nodes: leadingNodes, offset};
}
if (leadingNodes.length === 0) {
return {nodes: [], offset: -1};
}
// Returns all the nodes once we find a valid one among them.
for (const node of leadingNodes) {
if (NodeUtils.isValidLeafNode(node)) {
const lastLeadingNodeName =
ParagraphUtils.getNodeName(leadingNodes[leadingNodes.length - 1]);
return {nodes: leadingNodes, offset: lastLeadingNodeName.length};
}
}
return {nodes: [], offset: -1};
}
// If we search forward, we use the start node as anchor and look for
// remaining content. If we did not find a node for the charIndex, we use
// the last node of the current node group as the starting node and set
// offset to the length of the node's name. This enables us to get all the
// content within this paragraph but after the current node group.
if (!startNode) {
const lastNode = nodeGroup.nodes[nodeGroup.nodes.length - 1].node;
const lastChildOfLastNode = NodeUtils.getLastLeafChild(lastNode);
......
......@@ -832,44 +832,86 @@ TEST_F('SelectToSpeakNodeUtilsUnitTest', 'getAllNodesInParagraph', function() {
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroupWithOffset', function() {
'getNextNodesInParagraphFromNodeGroup_forward', function() {
// The nodeGroup has four inline text nodes and one static text node.
// Their starting indexes are 0, 9, 20, 30, and 51.
const nodeGroup = generateTestNodeGroup();
let result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 0 /* charIndex */);
let result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 0 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 5);
assertEquals(result.offset, 0);
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 5 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 5 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 5);
assertEquals(result.offset, 5);
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 9 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 9 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 4);
assertEquals(result.offset, 0);
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 25 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 25 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 3);
assertEquals(result.offset, 5);
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 51 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 51 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.offset, 0);
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 100 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 100 /* charIndex */,
constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 0);
});
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroupWithOffsetWithEmptyTail', function() {
'getNextNodesInParagraphFromNodeGroup_backward', function() {
// The nodeGroup has four inline text nodes and one static text node.
// Their starting indexes are 0, 9, 20, 30, and 51.
const nodeGroup = generateTestNodeGroup();
let result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 0 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 0);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 5 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.offset, 5);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 9 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.offset, 9);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 25 /* charIndex */,
constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 3);
assertEquals(result.offset, 5);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 51 /* charIndex */,
constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 4);
assertEquals(result.offset, 20);
// The charIndex is out of the range of nodeGroup. It will try to get all
// the content before the nodeGroup. In this case, there is nothing.
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 100 /* charIndex */,
constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 0);
});
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroup_forwardWithEmptyTail', function() {
// The nodeGroup consists of three inline text nodes: "Hello", "world ",
// and " ".
const nodeGroup = generateTestNodeGroupWithEmptyTail();
......@@ -881,21 +923,47 @@ TEST_F(
assertEquals(nodeWithOffset.node.name, 'world ');
assertEquals(nodeWithOffset.offset, 5);
let result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 11 /* charIndex */);
let result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 11 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 0);
// If we decrease the charIndex, we will get the node with 'world ' but
// not any empty node.
result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 10 /* charIndex */);
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 10 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.nodes[0].name, 'world ');
});
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroupWithOffsetFromPartialParagraph',
'getNextNodesInParagraphFromNodeGroup_backwardWithEmptyHeads', function() {
// The nodeGroup consists of three inline text nodes: " ", " Hello",
// "world".
const nodeGroup = generateTestNodeGroupWithEmptyHead();
// We can find the non-empty node at this charIndex but there is actually
// no text content before this position.
const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
nodeGroup, 2 /* charIndex */);
assertEquals(nodeWithOffset.node.name, ' Hello');
assertEquals(nodeWithOffset.offset, 1);
let result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 2 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 0);
// If we increase the charIndex, we will get the node with ' Hello' but
// not any empty node.
result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 3 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.nodes[0].name, ' Hello');
});
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroup_forwardFromPartialParagraph',
function() {
// The nodeGroup consists only one static text node, which is "one". The
// entire paragraph has three static text nodes: "Sentence", "one",
......@@ -908,12 +976,34 @@ TEST_F(
nodeGroup, 3 /* charIndex */);
assertEquals(nodeWithOffset.node, null);
const result = NodeUtils.getNextNodesInParagraphFromNodeGroupWithOffset(
nodeGroup, 3 /* charIndex */);
const result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 3 /* charIndex */, constants.Dir.FORWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.offset, 0);
});
TEST_F(
'SelectToSpeakNodeUtilsUnitTest',
'getNextNodesInParagraphFromNodeGroup_backwardFromPartialParagraph',
function() {
// The nodeGroup consists only one static text node, which is "one". The
// entire paragraph has three static text nodes: "Sentence", "one",
// "here".
const nodeGroup = generateTestNodeGroupFromPartialParagraph();
// After reading "one", the TTS events will set charIndex to 3. Finding
// the static text node from the node group will return null.
const nodeWithOffset = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
nodeGroup, 3 /* charIndex */);
assertEquals(nodeWithOffset.node, null);
const result = NodeUtils.getNextNodesInParagraphFromNodeGroup(
nodeGroup, 3 /* charIndex */, constants.Dir.BACKWARD /* direction */);
assertEquals(result.nodes.length, 1);
assertEquals(result.nodes[0].name, 'Sentence');
assertEquals(result.offset, 8);
});
/**
* Creates a AutomationNode-like object.
* @param {!Object} properties
......@@ -1019,6 +1109,31 @@ function generateTestNodeGroupWithEmptyTail() {
false /* do not split on language */);
}
/**
* Creates a nodeGroup that has an empty head (i.e., " Hello world").
* @return {!ParagraphUtils.NodeGroup}
*/
function generateTestNodeGroupWithEmptyHead() {
const root = createMockNode({role: 'rootWebArea'});
const paragraph =
createMockNode({role: 'paragraph', display: 'block', parent: root, root});
const text1 =
createMockNode({name: ' Hello', role: 'staticText', parent: paragraph});
const inlineText1 = createMockNode(
{role: 'inlineTextBox', name: ' ', indexInParent: 0, parent: text1});
const inlineText2 = createMockNode(
{role: 'inlineTextBox', name: ' Hello', indexInParent: 1, parent: text1});
const text2 =
createMockNode({name: 'world ', role: 'staticText', parent: paragraph});
const inlineText3 = createMockNode(
{role: 'inlineTextBox', name: 'world', indexInParent: 1, parent: text2});
return ParagraphUtils.buildNodeGroup(
[inlineText1, inlineText2, inlineText3], 0,
false /* do not split on language */);
}
/**
* Creates a nodeGroup that only has a part of the paragraph (e.g., the "one" in
* <p>Sentence <span>one</span> here</p>).
......
......@@ -316,15 +316,21 @@ class ParagraphUtils {
* @param {Array<!AutomationNode>} nodes List of automation nodes to use.
* @param {number} index The index into nodes at which to start.
* @param {{splitOnLanguage: (boolean|undefined),
* clipOverflowWords: (boolean|undefined)}=} opt_options
* splitOnLanguage: flag to determine if we should split nodes
* up based on language.
* splitOnParagraph: (boolean|undefined),
* clipOverflowWords: (boolean|undefined)}=} options
* splitOnLanguage: flag to determine if we should split nodes up based on
* language. If this is not passed, default to false.
* splitOnParagraph: flag to determine if we should split nodes up based
* on paragraph. If this is not passed, default to true.
* clipOverflowWords: Whether to clip generated text.
* @return {ParagraphUtils.NodeGroup} info about the node group
*/
static buildNodeGroup(nodes, index, opt_options) {
const options = opt_options || {};
static buildNodeGroup(nodes, index, options) {
options = options || {};
const splitOnLanguage = options.splitOnLanguage || false;
const splitOnParagraph = options.splitOnParagraph === undefined ?
true :
options.splitOnParagraph;
const clipOverflowWords = options.clipOverflowWords || false;
let node = nodes[index];
let next = nodes[index + 1];
......@@ -393,9 +399,10 @@ class ParagraphUtils {
// Stop if any of following is true:
// 1. we have no more nodes to process.
// 2. the next node is not part of the same paragraph.
// 2. we need to check for a paragraph split and if the next node is not
// part of the same paragraph.
if (index + 1 >= nodes.length ||
!ParagraphUtils.inSameParagraph(node, next)) {
(splitOnParagraph && !ParagraphUtils.inSameParagraph(node, next))) {
break;
}
......@@ -420,6 +427,59 @@ class ParagraphUtils {
return result;
}
/**
* Builds a single node group that contains all the input |nodes|. In
* addition, this function will transform the provided node offsets to values
* that are relative to the text of the resulting node group. This function
* can be used to check the sentence boundaries across all the input nodes.
* @param {!Array<!AutomationNode>} nodes The nodes for the selected content.
* @param {number=} opt_startIndex The index into the first node's text at
* which the selected content starts.
* @param {number=} opt_endIndex The index into the last node's text at which
* the selected content ends.
* @return {!{nodeGroup: ParagraphUtils.NodeGroup,
* startIndexInGroup: (number|undefined),
* endIndexInGroup: (number|undefined)}}
* nodeGroup: The node group that contains all the input |nodes|. The node
* group will not consider any language difference or paragraph split.
* startOffsetInGroup: The index into the node group's text at which the
* selected content starts.
* endOffsetInGroup: The index into the node group's text at which the
* selected content ends.
*/
static buildSingleNodeGroupWithOffset(nodes, opt_startIndex, opt_endIndex) {
const nodeGroup = ParagraphUtils.buildNodeGroup(
nodes, 0 /* index */,
{splitOnLanguage: false, splitOnParagraph: false});
if (opt_startIndex !== undefined) {
// 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
// parent). Thus, we need to adjust the |opt_startIndex|.
const firstNodeHasInlineText =
nodeGroup.nodes.length > 0 && nodeGroup.nodes[0].hasInlineText;
const startIndexInNodeParent = firstNodeHasInlineText ?
ParagraphUtils.getStartCharIndexInParent(nodes[0]) :
0;
opt_startIndex += startIndexInNodeParent + nodeGroup.nodes[0].startChar;
}
if (opt_endIndex !== undefined) {
// Similarly, |opt_endIndex| needs to be adjusted.
const lastNodeHasInlineText = nodeGroup.nodes.length > 0 &&
nodeGroup.nodes[nodeGroup.nodes.length - 1].hasInlineText;
const startIndexInNodeParent = lastNodeHasInlineText ?
ParagraphUtils.getStartCharIndexInParent(nodes[0]) :
0;
opt_endIndex += startIndexInNodeParent +
nodeGroup.nodes[nodeGroup.nodes.length - 1].startChar;
}
return {
nodeGroup,
startIndexInGroup: opt_startIndex,
endIndexInGroup: opt_endIndex
};
}
/**
* Finds the AutomationNode that appears at the given character index within
* the |nodeGroup|.
......
......@@ -247,6 +247,33 @@ TEST_F(
assertEquals(paragraph1, result.blockParent);
});
TEST_F(
'SelectToSpeakParagraphUnitTest', 'BuildNodeGroupAcrossParagraphs',
function() {
const root = {role: 'rootWebArea'};
const paragraph1 =
{role: 'paragraph', display: 'block', parent: root, root};
const text1 =
{role: 'staticText', parent: paragraph1, name: 'text1', root};
const text2 =
{role: 'staticText', parent: paragraph1, name: 'text2', root};
const paragraph2 =
{role: 'paragraph', display: 'block', parent: root, root};
const text3 =
{role: 'staticText', parent: paragraph2, name: 'text3', root};
const result = ParagraphUtils.buildNodeGroup(
[text1, text2, text3], 0, {splitOnParagraph: false});
assertEquals('text1 text2 text3 ', result.text);
assertEquals(2, result.endIndex);
assertEquals(3, result.nodes.length);
assertEquals(0, result.nodes[0].startChar);
assertEquals(text1, result.nodes[0].node);
assertEquals(6, result.nodes[1].startChar);
assertEquals(text2, result.nodes[1].node);
assertEquals(12, result.nodes[2].startChar);
assertEquals(text3, result.nodes[2].node);
});
TEST_F(
'SelectToSpeakParagraphUnitTest', 'BuildNodeGroupStopsAtLanguageBoundary',
function() {
......@@ -489,14 +516,17 @@ TEST_F(
'SelectToSpeakParagraphUnitTest', 'findNodeFromNodeGroupByCharIndex',
function() {
// The array has four inline text nodes and one static text node.
// Their starting indexes are 0, 9, 20, 30, and 51.
const nodeGroup = ParagraphUtils.buildNodeGroup(
generateNodesForParagraph(), 0, false /* do not split on language */);
const nodes = generateNodesForParagraph();
const nodeGroup =
ParagraphUtils.buildNodeGroup(generateNodesForParagraph(), 0);
// Start index = 0
const firstInline = 'The first';
// Start index = 9
const secondInline = ' sentence.';
// Start index = 20
const thirdInline = 'The second';
// Start index = 30
const fourthInline = ' sentence is longer.';
// Start index = 51
const thirdStatic = 'No child sentence.';
let result = ParagraphUtils.findNodeFromNodeGroupByCharIndex(
......@@ -534,6 +564,65 @@ TEST_F(
assertEquals(result.node, null);
});
TEST_F(
'SelectToSpeakParagraphUnitTest', 'BuildSingleNodeGroupWithOffset',
function() {
// The array has four inline text nodes and one static text node.
// Their starting indexes are 0, 9, 20, 30, and 51.
const nodes = generateNodesForParagraph();
// Start index = 0
const firstInline = 'The first';
// Start index = 9
const secondInline = ' sentence.';
const firstSentence = firstInline + secondInline + ' ';
// Start index = 20
const thirdInline = 'The second';
// Start index = 30
const fourthInline = ' sentence is longer.';
const secondSentence = thirdInline + fourthInline + ' ';
// Start index = 51
const thirdStatic = 'No child sentence.';
const thirdSentence = thirdStatic + ' ';
let nodeGroup, startIndexInGroup, endIndexInGroup;
({nodeGroup, startIndexInGroup, endIndexInGroup} =
ParagraphUtils.buildSingleNodeGroupWithOffset(nodes));
assertEquals(
nodeGroup.text, firstSentence + secondSentence + thirdSentence);
assertEquals(startIndexInGroup, undefined);
assertEquals(endIndexInGroup, undefined);
({nodeGroup, startIndexInGroup, endIndexInGroup} =
ParagraphUtils.buildSingleNodeGroupWithOffset(
nodes, 5 /* startIndex */));
assertEquals(
nodeGroup.text, firstSentence + secondSentence + thirdSentence);
assertEquals(startIndexInGroup, 5);
assertEquals(endIndexInGroup, undefined);
({nodeGroup, startIndexInGroup, endIndexInGroup} =
ParagraphUtils.buildSingleNodeGroupWithOffset(
nodes.slice(1), 0 /* startIndex */));
assertEquals(
nodeGroup.text, firstSentence + secondSentence + thirdSentence);
assertEquals(startIndexInGroup, 9);
assertEquals(endIndexInGroup, undefined);
({nodeGroup, startIndexInGroup, endIndexInGroup} =
ParagraphUtils.buildSingleNodeGroupWithOffset(
nodes.slice(2, 5), 1 /* startIndex */, 1 /* endIndex */));
assertEquals(nodeGroup.text, secondSentence + thirdSentence);
assertEquals(startIndexInGroup, 1);
assertEquals(endIndexInGroup, 32);
({nodeGroup, startIndexInGroup, endIndexInGroup} =
ParagraphUtils.buildSingleNodeGroupWithOffset(
nodes.slice(4, 5), undefined, 5 /* endIndex */));
assertEquals(nodeGroup.text, thirdSentence);
assertEquals(startIndexInGroup, undefined);
assertEquals(endIndexInGroup, 5);
});
/**
* Creates an array of nodes that represents a paragraph.
* @return {Array<AutomationNode>}
......
......@@ -276,6 +276,68 @@ TEST_F('SelectToSpeakNavigationControlTest', 'NextSentence', function() {
});
});
TEST_F(
'SelectToSpeakNavigationControlTest', 'NextSentenceWithinParagraph',
function() {
const bodyHtml = `
<p id="p1">Sent 1. <span id="s1">Sent 2.</span> Sent 3. Sent 4.</p>
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
this.triggerReadSelectedText();
// Speaks the first word.
this.mockTts.speakUntilCharIndex(5);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 2.');
// Hitting next sentence will start from the next sentence.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction
.NEXT_SENTENCE);
this.waitOneEventLoop(() => {
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 3. Sent 4.');
});
});
});
TEST_F(
'SelectToSpeakNavigationControlTest', 'NextSentenceAcrossParagraph',
function() {
const bodyHtml = `
<p id="p1">Sent 1.</p>
<p id="p2">Sent 2. Sent 3.</p>'
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
this.triggerReadSelectedText();
// Speaks the first word.
this.mockTts.speakUntilCharIndex(5);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 1.');
// Hitting next sentence will star from the next paragraph as there
// is no more sentence in the current paragraph.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction
.NEXT_SENTENCE);
this.waitOneEventLoop(() => {
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 2. Sent 3.');
});
});
});
TEST_F('SelectToSpeakNavigationControlTest', 'PrevSentence', function() {
const bodyHtml = `
<p id="p1">First sentence. Second sentence. Third sentence.</p>'
......@@ -304,6 +366,69 @@ TEST_F('SelectToSpeakNavigationControlTest', 'PrevSentence', function() {
});
});
TEST_F(
'SelectToSpeakNavigationControlTest', 'PrevSentenceWithinParagraph',
function() {
const bodyHtml = `
<p id="p1">Sent 0. Sent 1. <span id="s1">Sent 2.</span> Sent 3.</p>
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
this.triggerReadSelectedText();
// Supposing we are at the start of the sentence.
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 2.');
// Hitting previous sentence will start from the previous sentence.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction
.PREVIOUS_SENTENCE);
this.waitOneEventLoop(() => {
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0],
'Sent 1. Sent 2. Sent 3.');
});
});
});
TEST_F(
'SelectToSpeakNavigationControlTest', 'PrevSentenceAcrossParagraph',
function() {
const bodyHtml = `
<p id="p1">Sent 1. Sent 2.</p>
<p id="p2">Sent 3.</p>'
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p2', bodyHtml), () => {
this.triggerReadSelectedText();
// We are at the start of the sentence.
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 3.');
// Hitting previous sentence will start from the last sentence in
// the previous paragraph as there is no more sentence in the
// current paragraph.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction
.PREVIOUS_SENTENCE);
this.waitOneEventLoop(() => {
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Sent 2.');
});
});
});
// TODO(https://crbug.com/1157817): Fix Flaky Test.
TEST_F(
'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePlaying',
function() {
......
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