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

Resume from the beginning of the sentence in Select To Speak

When a user resumes from a pause, STS will speak from the beginning of the current sentence. If the user paused on a sentence start, STS will continue reading from the pause.

This CL also refactors the functions in sentence_utils.js and uses staticTextNode directly to query sentenceStarts.

AX-Relnotes: N/A

Bug: 1143817
Change-Id: Iaf73a5656273db499604d3f7f4a008c37e5a4eb6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2572905
Commit-Queue: Lei Shi <leileilei@google.com>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#835806}
parent 67a6625b
......@@ -632,14 +632,28 @@ class SelectToSpeak {
}
/**
* Resume the TTS. Currently, we just ask the TTS engine to pick up where it
* quited last time in |this.startCurrentNodeGroup_|.
* Resume the TTS from the beginning of the current sentence.
* @private
*/
resume_() {
if (this.isPaused_()) {
this.startCurrentNodeGroup_();
// If TTS is not paused, return early.
if (!this.isPaused_()) {
return;
}
if (!SentenceUtils.isSentenceStart(
this.currentNodeGroup_, this.navigationState_.currentCharIndex)) {
// If the current position is not a sentence start, move to the start of
// the current sentence.
const currentSentenceStart = SentenceUtils.getSentenceStart(
this.currentNodeGroup_, this.navigationState_.currentCharIndex,
constants.Dir.BACKWARD);
// Only navigate to the current sentence start if it exists, otherwise
// move to the beginning of the current nodeGroup.
// TODO(leileilei): check if the 0 char index would cause issues.
this.navigationState_.currentCharIndex =
currentSentenceStart === null ? 0 : currentSentenceStart;
}
this.startCurrentNodeGroup_();
}
/**
......
......@@ -146,36 +146,109 @@ TEST_F(
});
});
TEST_F('SelectToSpeakNavigationControlTest', 'PauseAndResume', function() {
const bodyHtml = `
<p id="p1">Paragraph 1</p>'
TEST_F(
'SelectToSpeakNavigationControlTest', 'PauseResumeWithinTheSentence',
function() {
const bodyHtml = `
<p id="p1">First sentence. Second sentence. Third sentence.</p>'
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
this.triggerReadSelectedText();
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
this.triggerReadSelectedText();
// Speaks the first word.
this.mockTts.speakUntilCharIndex(10);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Paragraph 1');
// Speaks until the second word of the second sentence.
this.mockTts.speakUntilCharIndex(23);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0],
'First sentence. Second sentence. Third sentence.');
// Hitting pause will stop the current TTS.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.PAUSE);
assertFalse(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 0);
// Hitting pause will stop the current TTS.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.PAUSE);
assertFalse(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 0);
// Hitting resume will start from the beginning of the second
// sentence.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.RESUME);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0],
'Second sentence. Third sentence.');
});
});
// Hitting resume will start from the next position.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.RESUME);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], '1');
});
});
TEST_F(
'SelectToSpeakNavigationControlTest', 'PauseResumeAtTheBeginningOfSentence',
function() {
const bodyHtml = `
<p id="p1">First sentence. Second sentence. Third sentence.</p>'
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
this.triggerReadSelectedText();
// Speaks until the third sentence.
this.mockTts.speakUntilCharIndex(33);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0],
'First sentence. Second sentence. Third sentence.');
// Hitting pause will stop the current TTS.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.PAUSE);
assertFalse(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 0);
// Hitting resume will start from the beginning of the third
// sentence.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.RESUME);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'Third sentence.');
});
});
TEST_F(
'SelectToSpeakNavigationControlTest',
'PauseResumeAtTheBeginningOfParagraph', function() {
const bodyHtml = `
<p id="p1">first sentence.</p>'
`;
this.runWithLoadedTree(
this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
this.triggerReadSelectedText();
// Speaks until the second word.
this.mockTts.speakUntilCharIndex(6);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'first sentence.');
// Hitting pause will stop the current TTS.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.PAUSE);
assertFalse(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 0);
// Hitting resume will start from the beginning of the paragraph.
selectToSpeak.onSelectToSpeakPanelAction_(
chrome.accessibilityPrivate.SelectToSpeakPanelAction.RESUME);
assertTrue(this.mockTts.currentlySpeaking());
assertEquals(this.mockTts.pendingUtterances().length, 1);
this.assertEqualsCollapseWhitespace(
this.mockTts.pendingUtterances()[0], 'first sentence.');
});
});
TEST_F('SelectToSpeakNavigationControlTest', 'NextSentence', function() {
const bodyHtml = `
......
......@@ -211,79 +211,106 @@ TEST_F(
constants.Dir.BACKWARD /* direction */));
});
TEST_F('SelectToSpeakSentenceUtilsUnitTest', 'isSentenceStart', function() {
// The text of the test node group is "Hello. New. World."
const nodeGroup = getTestNodeGroupWithOneNode();
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 0 /* startCharIndex */));
assertEquals(
false,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 3 /* startCharIndex */));
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 7 /* startCharIndex */));
assertEquals(
false,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 11 /* startCharIndex */));
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 12 /* startCharIndex */));
});
TEST_F(
'SelectToSpeakSentenceUtilsUnitTest', 'isSentenceStartMultiNodes',
function() {
// The text of the test node group is "Hello. New. Beautiful. World." The
// char indexes of four sentence starts are 0, 7, 12, 23.
const nodeGroup = getTestNodeGroupWithMultiNodes();
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 0 /* startCharIndex */));
assertEquals(
false,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 3 /* startCharIndex */));
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 7 /* startCharIndex */));
assertEquals(
false,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 11 /* startCharIndex */));
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 12 /* startCharIndex */));
assertEquals(
false,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 15 /* startCharIndex */));
assertEquals(
true,
SentenceUtils.isSentenceStart(
nodeGroup /* nodeGroup */, 23 /* startCharIndex */));
});
function getTestNodeGroupWithOneNode() {
const inlineText = {sentenceStarts: [0, 7, 12], name: 'Hello. New. World.'};
const staticText = {children: [inlineText], name: 'Hello. New. World.'};
const node = {node: staticText, startChar: 0, hasInlineText: true};
const staticText = {sentenceStarts: [0, 7, 12], name: 'Hello. New. World.'};
const node = {node: staticText, startChar: 0};
return {nodes: [node], text: 'Hello. New. World.'};
}
function getTestNodeGroupWithMultiNodes() {
const staticText1 = {name: 'Hello. New. ', role: 'staticText'};
const inlineText1 = {
sentenceStarts: [0, 7],
const staticText1 = {
name: 'Hello. New. ',
indexInParent: 0,
parent: staticText1
role: 'staticText',
sentenceStarts: [0, 7]
};
staticText1.children = [inlineText1];
const node1 = {node: staticText1, startChar: 0, hasInlineText: true};
const node1 = {node: staticText1, startChar: 0};
const staticText2 = {name: 'Beautiful. World.', role: 'staticText'};
const inlineText2 = {
sentenceStarts: [0],
name: 'Beautiful. ',
indexInParent: 0,
parent: staticText2
};
const inlineText3 = {
sentenceStarts: [0],
name: 'World.',
indexInParent: 1,
parent: staticText2
const staticText2 = {
name: 'Beautiful. World.',
role: 'staticText',
sentenceStarts: [0, 11]
};
staticText2.children = [inlineText2, inlineText3];
const node2 = {node: staticText2, startChar: 12, hasInlineText: true};
const node2 = {node: staticText2, startChar: 12};
return {nodes: [node1, node2], text: 'Hello. New. Beautiful. World.'};
}
function getTestNodeGroupWithSentenceSpanningAcrossMultiNodes() {
const staticText1 = {name: 'Hello', role: 'staticText'};
const inlineText1 = {
sentenceStarts: [0],
name: 'Hello',
indexInParent: 0,
parent: staticText1
};
staticText1.children = [inlineText1];
const node1 = {node: staticText1, startChar: 0, hasInlineText: true};
const staticText1 = {name: 'Hello', role: 'staticText', sentenceStarts: [0]};
const node1 = {node: staticText1, startChar: 0};
const staticText2 = {name: ' world. New', role: 'staticText'};
const inlineText2 = {
sentenceStarts: [],
name: ' world.',
indexInParent: 0,
parent: staticText2
const staticText2 = {
name: ' world. New',
role: 'staticText',
sentenceStarts: [8]
};
const inlineText3 = {
sentenceStarts: [1],
name: ' New',
indexInParent: 1,
parent: staticText2
};
staticText2.children = [inlineText2, inlineText3];
const node2 = {node: staticText2, startChar: 5, hasInlineText: true};
const node2 = {node: staticText2, startChar: 5};
const staticText3 = {name: ' world.', role: 'staticText'};
const inlineText4 = {
sentenceStarts: [],
name: ' world.',
indexInParent: 0,
parent: staticText3
};
staticText3.children = [inlineText4];
const node3 = {node: staticText3, startChar: 16, hasInlineText: true};
const staticText3 = {name: ' world.', role: 'staticText', sentenceStarts: []};
const node3 = {node: staticText3, startChar: 16};
return {nodes: [node1, node2, node3], text: 'Hello world. New world.'};
}
\ 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