From e385f3369ac6d64ad242ae58abbf471ea29a6668 Mon Sep 17 00:00:00 2001 From: Ondrej Skopek Date: Mon, 4 Sep 2017 16:49:32 +0000 Subject: [PATCH] [Local NTP Voice] JavaScript browser tests: Text module. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds JavaScript browser tests for the text module of Voice Search on the Local NTP. Tests are ported from text_view_test.js of the remote NTP's Voice Search module. Bug: 583291 Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation Change-Id: Ieb403144460d3a8e327d341855ca8e692fee6ede Reviewed-on: https://chromium-review.googlesource.com/638332 Commit-Queue: Ondrej Škopek Reviewed-by: Marc Treib Cr-Commit-Position: refs/heads/master@{#499512} --- chrome/browser/resources/local_ntp/voice.css | 3 +- chrome/browser/resources/local_ntp/voice.js | 79 ++++-- .../ui/search/local_ntp_browsertest.cc | 11 + .../data/local_ntp/voice_browsertest.html | 9 + .../data/local_ntp/voice_text_browsertest.js | 252 ++++++++++++++++++ 5 files changed, 324 insertions(+), 30 deletions(-) create mode 100644 chrome/test/data/local_ntp/voice_text_browsertest.js diff --git a/chrome/browser/resources/local_ntp/voice.css b/chrome/browser/resources/local_ntp/voice.css index 37f783b1623af..caf5f0df742ed 100644 --- a/chrome/browser/resources/local_ntp/voice.css +++ b/chrome/browser/resources/local_ntp/voice.css @@ -216,7 +216,6 @@ /* TEXT */ /* Classes: * - voice-text - Text area style class - * - voice-text-area - Link style class * - voice-text-2l - 2 line style class * - voice-text-3l - 3 line style class * - voice-text-4l - 4 line style class @@ -279,7 +278,7 @@ } /* Text area links. */ -#voice-text-area { +.voice-text-link { color: var(--text_link_color); cursor: pointer; font-size: 18px; diff --git a/chrome/browser/resources/local_ntp/voice.js b/chrome/browser/resources/local_ntp/voice.js index 3e8623c89d55e..37ef72392b115 100644 --- a/chrome/browser/resources/local_ntp/voice.js +++ b/chrome/browser/resources/local_ntp/voice.js @@ -834,13 +834,18 @@ speech.toggleStartStop = function() { * Handles click events during speech recognition. * @param {boolean} shouldSubmit True if a query should be submitted. * @param {boolean} shouldRetry True if the interface should be restarted. + * @param {boolean} navigatingAway True if the browser is navigating away + * from the NTP. * @private */ -speech.onClick_ = function(shouldSubmit, shouldRetry) { +speech.onClick_ = function(shouldSubmit, shouldRetry, navigatingAway) { if (speech.finalResult_ && shouldSubmit) { speech.submitFinalResult_(); } else if (speech.currentState_ == speech.State_.STOPPED && shouldRetry) { speech.restart(); + } else if (speech.currentState_ == speech.State_.STOPPED && navigatingAway) { + // If the user clicks on a "Learn more" or "Details" support page link + // from an error message, do nothing, and let Chrome navigate to that page. } else { speech.abort_(); } @@ -865,10 +870,24 @@ let text = {}; /** - * ID for the link shown in error output. + * ID for the "Try Again" link shown in error output. * @const */ -text.ERROR_LINK_ID = 'voice-text-area'; +text.RETRY_LINK_ID = 'voice-retry-link'; + + +/** + * ID for the Voice Search support site link shown in error output. + * @const + */ +text.SUPPORT_LINK_ID = 'voice-support-link'; + + +/** + * Class for the links shown in error output. + * @const + */ +text.ERROR_LINK_CLASS_ = 'voice-text-link'; /** @@ -993,21 +1012,22 @@ text.updateTextArea = function(interimText, opt_finalText) { /** - * Sets the text view to the initializing state. + * Sets the text view to the initializing state. The initializing message + * shown while waiting for permission is not displayed immediately, but after + * a short timeout. The reason for this is that the "Waiting..." message would + * still appear ("blink") every time a user opens Voice Search, even if they + * have already granted and persisted microphone permission for the NTP, + * and could therefore directly proceed to the "Speak now" message. */ text.showInitializingMessage = function() { + text.interim_.textContent = ''; + text.final_.textContent = ''; + const displayMessage = function() { - if (text.interim_.innerText == '') { + if (text.interim_.textContent == '') { text.updateTextArea(speech.messages.waiting); } }; - - text.interim_.textContent = ''; - text.final_.textContent = ''; - - // We give the interface some time to get the permission. Once permission - // is obtained, the ready message is displayed, in which case the - // initializing message won't be shown. text.initializingTimer_ = window.setTimeout(displayMessage, text.INITIALIZING_TIMEOUT_MS_); }; @@ -1024,6 +1044,7 @@ text.showReadyMessage = function() { /** + * Display an error message in the text area for the given error. * @param {RecognitionError} error The error that occured. */ text.showErrorMessage = function(error) { @@ -1071,21 +1092,24 @@ text.getErrorMessage_ = function(error) { */ text.getErrorLink_ = function(error) { let linkElement = document.createElement('a'); - linkElement.id = text.ERROR_LINK_ID; + linkElement.className = text.ERROR_LINK_CLASS_; switch (error) { case RecognitionError.NO_MATCH: + linkElement.id = text.RETRY_LINK_ID; linkElement.textContent = speech.messages.tryAgain; linkElement.onclick = speech.restart; return linkElement; case RecognitionError.NO_SPEECH: case RecognitionError.AUDIO_CAPTURE: + linkElement.id = text.SUPPORT_LINK_ID; linkElement.href = text.SUPPORT_LINK_BASE_ + getChromeUILanguage(); linkElement.textContent = speech.messages.learnMore; linkElement.target = '_blank'; return linkElement; case RecognitionError.NOT_ALLOWED: case RecognitionError.SERVICE_NOT_ALLOWED: + linkElement.id = text.SUPPORT_LINK_ID; linkElement.href = text.SUPPORT_LINK_BASE_ + getChromeUILanguage(); linkElement.textContent = speech.messages.details; linkElement.target = '_blank'; @@ -1100,6 +1124,8 @@ text.getErrorLink_ = function(error) { * Clears the text elements. */ text.clear = function() { + text.updateTextArea(''); + text.cancelListeningTimeout(); window.clearTimeout(text.initializingTimer_); @@ -1152,7 +1178,9 @@ text.getTextClassName_ = function() { */ text.startListeningMessageAnimation_ = function() { const animateListeningText = function() { - if (text.interim_.innerText == speech.messages.ready) { + // TODO(oskopek): Substitute the fragile string comparison with a correct + // state condition. + if (text.interim_.textContent == speech.messages.ready) { text.updateTextArea(speech.messages.listening); text.interim_.classList.add(text.LISTENING_ANIMATION_CLASS_); } @@ -1327,13 +1355,6 @@ view.OVERLAY_HIDDEN_CLASS_ = 'overlay-hidden'; view.BACKGROUND_ID_ = 'voice-overlay'; -/** - * ID of the close (x) button. - * @const @private - */ -view.CLOSE_BUTTON_ID_ = 'voice-close-button'; - - /** * ID for the speech output container. * @const @private @@ -1550,12 +1571,14 @@ view.onWindowClick_ = function(event) { if (!view.isVisible_) { return; } - const targetId = event.target.id; - const shouldRetry = (targetId == microphone.RED_BUTTON_ID || - targetId == text.ERROR_LINK_ID) && - view.isNoMatchShown_; - const submitQuery = - targetId == microphone.RED_BUTTON_ID && !view.isNoMatchShown_; - view.onClick_(submitQuery, shouldRetry); + const retryLinkClicked = event.target.id === text.RETRY_LINK_ID; + const supportLinkClicked = event.target.id === text.SUPPORT_LINK_ID; + const micIconClicked = event.target.id === microphone.RED_BUTTON_ID; + + const submitQuery = micIconClicked && !view.isNoMatchShown_; + const shouldRetry = + retryLinkClicked || (micIconClicked && view.isNoMatchShown_); + const navigatingAway = supportLinkClicked; + view.onClick_(submitQuery, shouldRetry, navigatingAway); }; /* END VIEW */ diff --git a/chrome/browser/ui/search/local_ntp_browsertest.cc b/chrome/browser/ui/search/local_ntp_browsertest.cc index 1fc8d2d261a40..88032726c7269 100644 --- a/chrome/browser/ui/search/local_ntp_browsertest.cc +++ b/chrome/browser/ui/search/local_ntp_browsertest.cc @@ -350,6 +350,17 @@ IN_PROC_BROWSER_TEST_F(LocalNTPVoiceJavascriptTest, MicrophoneTests) { EXPECT_TRUE(success); } +IN_PROC_BROWSER_TEST_F(LocalNTPVoiceJavascriptTest, TextTests) { + content::WebContents* active_tab = + OpenNewTab(browser(), GURL(chrome::kChromeUINewTabURL)); + + // Run the tests. + bool success = false; + ASSERT_TRUE(instant_test_utils::GetBoolFromJS( + active_tab, "!!runSimpleTests('text')", &success)); + EXPECT_TRUE(success); +} + namespace { // Returns the RenderFrameHost corresponding to the most visited iframe in the diff --git a/chrome/test/data/local_ntp/voice_browsertest.html b/chrome/test/data/local_ntp/voice_browsertest.html index 7df0b084695d3..4bb4d5f569513 100644 --- a/chrome/test/data/local_ntp/voice_browsertest.html +++ b/chrome/test/data/local_ntp/voice_browsertest.html @@ -9,6 +9,7 @@ + + diff --git a/chrome/test/data/local_ntp/voice_text_browsertest.js b/chrome/test/data/local_ntp/voice_text_browsertest.js new file mode 100644 index 0000000000000..acfafdc157259 --- /dev/null +++ b/chrome/test/data/local_ntp/voice_text_browsertest.js @@ -0,0 +1,252 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + + +/** + * @fileoverview Tests the text module of Voice Search on the local NTP. + */ + + +/** + * Voice Search Text module's object for test and setup functions. + */ +test.text = {}; + + +/** + * Utility to test code that uses timeouts. + * @type {MockClock} + */ +test.text.clock = new MockClock(); + + +/** + * Utility to mock out object properties. + * @type {Replacer} + */ +test.text.stubs = new Replacer(); + + +/** + * Set up the text DOM and test environment. + */ +test.text.setUp = function() { + test.text.clock.reset(); + test.text.stubs.reset(); + + setUpPage('voice-text-template'); + + test.text.clock.install(); + test.text.stubs.replace(window, 'getChromeUILanguage', () => 'en-ZA'); + test.text.stubs.replace(speech, 'messages', { + audioError: 'Audio error', + details: 'Details', + languageError: 'Language error', + learnMore: 'Learn more', + listening: 'Listening', + networkError: 'Network error', + noTranslation: 'No translation', + noVoice: 'No voice', + permissionError: 'Permission error', + ready: 'Ready', + tryAgain: 'Try again', + waiting: 'Waiting' + }); + + text.init(); +}; + + +/** + * Makes sure text sets up with the correct settings. + */ +test.text.testInit = function() { + assertEquals('', text.interim_.textContent); + assertEquals('', text.final_.textContent); +}; + + +/** + * Test updating the text values. + */ +test.text.testUpdateText = function() { + const interimText = 'interim'; + const finalText = 'final'; + text.updateTextArea(interimText, finalText); + assertEquals(interimText, text.interim_.textContent); + assertEquals(finalText, text.final_.textContent); +}; + + +/** + * Test updating the text with an error message containing a link. + */ +test.text.testShowErrorMessageWithLink = function() { + const noAudioError = RecognitionError.AUDIO_CAPTURE; + text.showErrorMessage(noAudioError); + const supportLink = text.SUPPORT_LINK_BASE_.replace(/&/, '&') + 'en-ZA'; + assertEquals( + 'Audio error Learn more`, + text.interim_.innerHTML); + assertEquals('', text.final_.innerHTML); +}; + + +/** + * Test updating the text with an error message containing a callback. + */ +test.text.testShowErrorMessageWithCallback = function() { + // Mock the restart callback. + let restartCalled = false; + test.text.stubs.replace(speech, 'restart', function() { + restartCalled = true; + }); + + // Display the try again error. + const tryAgainError = RecognitionError.NO_MATCH; + text.showErrorMessage(tryAgainError); + assertEquals( + 'No translation ' + + 'Try again', + text.interim_.innerHTML); + assertEquals('', text.final_.innerHTML); + + // Assert the callback is called when the link element is clicked. + assert(!restartCalled); + assertEquals(1, text.interim_.children.length); + text.interim_.children[0].click(); + assert(restartCalled); +}; + + +/** + * Test clearing the text elements. + */ +test.text.testClearText = function() { + const interimText = 'interim '.repeat(100); + const finalText = 'final '.repeat(100); + + assertEquals('voice-text', text.interim_.className); + assertEquals('voice-text', text.final_.className); + text.updateTextArea(interimText, finalText); + assertEquals(interimText, text.interim_.textContent); + assertEquals(finalText, text.final_.textContent); + assertEquals('voice-text voice-text-3l', text.interim_.className); + assertEquals('voice-text voice-text-3l', text.final_.className); + + text.clear(); + assertEquals('', text.interim_.textContent); + assertEquals('', text.final_.textContent); + assertEquals('voice-text', text.interim_.className); + assertEquals('voice-text', text.final_.className); +}; + + +/** + * Test showing the initialization message after an initial timeout. + */ +test.text.testSetInitializationMessage = function() { + text.interim_.textContent = 'interim text'; + text.final_.textContent = 'final text'; + + test.text.clock.setTime(1); + text.showInitializingMessage(); + assertEquals('', text.interim_.textContent); + assertEquals('', text.final_.textContent); + assertEquals(1, test.text.clock.pendingTimeouts.length); + assertEquals(301, test.text.clock.pendingTimeouts[0].activationTime); + + test.text.clock.advanceTime(300); + test.text.clock.pendingTimeouts.shift().callback(); + + assertEquals('Waiting', text.interim_.textContent); + assertEquals('', text.final_.textContent); + assertEquals(0, test.text.clock.pendingTimeouts.length); +}; + + +/** + * Test showing the ready message. + */ +test.text.testReadyMessage = function() { + text.interim_.textContent = 'interim text'; + text.final_.textContent = 'final text'; + + test.text.clock.setTime(1); + text.showReadyMessage(); + + assertEquals('Ready', text.interim_.textContent); + assertEquals('', text.final_.textContent); + + // Assert that the "Listening..." message will be shown after some time. + assertEquals(1, test.text.clock.pendingTimeouts.length); + assertEquals(2001, test.text.clock.pendingTimeouts[0].activationTime); +}; + + +/** + * Test showing the listening message when the ready message is shown. + */ +test.text.testListeningMessageWhenReady = function() { + text.interim_.textContent = 'Ready'; + + test.text.clock.setTime(1); + text.startListeningMessageAnimation_(); + + assertEquals(1, test.text.clock.pendingTimeouts.length); + assertEquals(2001, test.text.clock.pendingTimeouts[0].activationTime); + + test.text.clock.advanceTime(2000); + test.text.clock.pendingTimeouts.shift().callback(); + + assertEquals('Listening', text.interim_.textContent); + assertEquals('', text.final_.textContent); + assertEquals(0, test.text.clock.pendingTimeouts.length); +}; + + +/** + * Test not showing the listening message when the ready message is not shown. + */ +test.text.testListeningMessageWhenNotReady = function() { + text.interim_.textContent = 'some text'; + + test.text.clock.setTime(1); + text.startListeningMessageAnimation_(); + + assertEquals(1, test.text.clock.pendingTimeouts.length); + assertEquals(2001, test.text.clock.pendingTimeouts[0].activationTime); + + test.text.clock.advanceTime(2000); + test.text.clock.pendingTimeouts.shift().callback(); + + assertEquals('some text', text.interim_.textContent); + assertEquals('', text.final_.textContent); + assertEquals(0, test.text.clock.pendingTimeouts.length); +}; + +/** + * Test not showing the listening message when the ready message is spoken. + */ +test.text.testListeningMessageWhenNotReady = function() { + // Show the "Ready" message. + text.interim_.textContent = 'Ready'; + assertEquals('', text.final_.textContent); + + // Set the "Listening..." timeout. + test.text.clock.setTime(1); + text.startListeningMessageAnimation_(); + assertEquals(1, test.text.clock.pendingTimeouts.length); + assertEquals(2001, test.text.clock.pendingTimeouts[0].activationTime); + + // Simulate the user speaking the exact same "Ready" message. + test.text.clock.advanceTime(1000); + text.updateTextArea('Ready'); + assertEquals('Ready', text.interim_.textContent); + assertEquals('', text.final_.textContent); + + // The "Listening..." timeout gets cleared and the message will not show up. + assertEquals(0, test.text.clock.pendingTimeouts.length); +};