| // Copyright 2018 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. |
| |
| #include "chrome/browser/ash/accessibility/dictation.h" |
| |
| #include <memory> |
| |
| #include "ash/accessibility/accessibility_controller_impl.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/public/cpp/system_tray_test_api.h" |
| #include "ash/shell.h" |
| #include "base/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/metrics_hashes.h" |
| #include "base/metrics/statistics_recorder.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/timer/timer.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/ash/accessibility/accessibility_manager.h" |
| #include "chrome/browser/ash/accessibility/accessibility_test_utils.h" |
| #include "chrome/browser/ash/accessibility/dictation_bubble_test_helper.h" |
| #include "chrome/browser/ash/accessibility/speech_monitor.h" |
| #include "chrome/browser/ash/input_method/textinput_test_helper.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/speech/speech_recognition_constants.h" |
| #include "chrome/browser/speech/speech_recognition_test_helper.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/interactive_test_utils.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/metrics/content/subprocess_metrics_provider.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/soda/soda_installer.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/accessibility_notification_waiter.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/fake_speech_recognition_manager.h" |
| #include "extensions/browser/browsertest_util.h" |
| #include "extensions/browser/extension_host_test_helper.h" |
| #include "media/mojo/mojom/speech_recognition_service.mojom.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/clipboard/clipboard.h" |
| #include "ui/base/clipboard/clipboard_buffer.h" |
| #include "ui/base/clipboard/clipboard_monitor.h" |
| #include "ui/base/clipboard/clipboard_observer.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/base/ime/ash/mock_ime_input_context_handler.h" |
| #include "ui/base/ime/fake_text_input_client.h" |
| #include "ui/base/ime/input_method_base.h" |
| #include "ui/events/test/event_generator.h" |
| |
| namespace ash { |
| namespace { |
| |
| constexpr int kPrintErrorMessageDelayMs = 3500; |
| |
| const char kFirstSpeechResult[] = "Help"; |
| const char16_t kFirstSpeechResult16[] = u"Help"; |
| const char kFinalSpeechResult[] = "Hello world"; |
| const char16_t kFinalSpeechResult16[] = u"Hello world"; |
| const char16_t kTrySaying[] = u"Try saying:"; |
| const char16_t kType[] = u"\"Type [word / phrase]\""; |
| const char16_t kHelp[] = u"\"Help\""; |
| const char16_t kUndo[] = u"\"Undo\""; |
| const char16_t kDelete[] = u"\"Delete\""; |
| const char16_t kSelectAll[] = u"\"Select all\""; |
| const char16_t kUnselect[] = u"\"Unselect\""; |
| const char16_t kCopy[] = u"\"Copy\""; |
| const char* kOnDeviceListeningDurationMetric = |
| "Accessibility.CrosDictation.ListeningDuration.OnDeviceRecognition"; |
| const char* kNetworkListeningDurationMetric = |
| "Accessibility.CrosDictation.ListeningDuration.NetworkRecognition"; |
| const char* kLocaleMetric = "Accessibility.CrosDictation.Language"; |
| const char* kOnDeviceSpeechMetric = |
| "Accessibility.CrosDictation.UsedOnDeviceSpeech"; |
| const char* kMacroRecognizedMetric = |
| "Accessibility.CrosDictation.MacroRecognized"; |
| const char* kMacroSucceededMetric = |
| "Accessibility.CrosDictation.MacroSucceeded"; |
| const char* kMacroFailedMetric = "Accessibility.CrosDictation.MacroFailed"; |
| const int kInputTextViewMetricValue = 1; |
| |
| static const char* kEnglishDictationCommands[] = { |
| "delete", |
| "move to the previous character", |
| "move to the next character", |
| "move to the previous line", |
| "move to the next line", |
| "copy", |
| "paste", |
| "cut", |
| "undo", |
| "redo", |
| "select all", |
| "unselect", |
| "help", |
| "new line", |
| "cancel", |
| "delete the previous word", |
| "delete the previous sentence", |
| "move to the next word", |
| "move to the previous word", |
| "delete phrase", |
| "replace phrase with another phrase", |
| "insert phrase before another phrase", |
| "select from phrase to another phrase", |
| "move to the next sentence", |
| "move to the previous sentence"}; |
| |
| PrefService* GetActiveUserPrefs() { |
| return ProfileManager::GetActiveUserProfile()->GetPrefs(); |
| } |
| |
| AccessibilityManager* GetManager() { |
| return AccessibilityManager::Get(); |
| } |
| |
| void EnableChromeVox() { |
| GetManager()->EnableSpokenFeedback(true); |
| } |
| |
| std::string ToString(DictationBubbleIconType icon) { |
| switch (icon) { |
| case DictationBubbleIconType::kHidden: |
| return "hidden"; |
| case DictationBubbleIconType::kStandby: |
| return "standby"; |
| case DictationBubbleIconType::kMacroSuccess: |
| return "macro success"; |
| case DictationBubbleIconType::kMacroFail: |
| return "macro fail"; |
| } |
| } |
| |
| // Listens for changes to the histogram provided at construction. This class |
| // only allows `Wait()` to be called once. If you need to call `Wait()` multiple |
| // times, create multiple instances of this class. |
| class HistogramWaiter { |
| public: |
| explicit HistogramWaiter(const char* metric_name) { |
| histogram_observer_ = std::make_unique< |
| base::StatisticsRecorder::ScopedHistogramSampleObserver>( |
| metric_name, base::BindRepeating(&HistogramWaiter::OnHistogramCallback, |
| base::Unretained(this))); |
| } |
| ~HistogramWaiter() { histogram_observer_.reset(); } |
| |
| HistogramWaiter(const HistogramWaiter&) = delete; |
| HistogramWaiter& operator=(const HistogramWaiter&) = delete; |
| |
| // Waits for the next update to the observed histogram. |
| void Wait() { run_loop_.Run(); } |
| |
| void OnHistogramCallback(const char* metric_name, |
| uint64_t name_hash, |
| base::HistogramBase::Sample sample) { |
| run_loop_.Quit(); |
| histogram_observer_.reset(); |
| } |
| |
| private: |
| std::unique_ptr<base::StatisticsRecorder::ScopedHistogramSampleObserver> |
| histogram_observer_; |
| base::RunLoop run_loop_; |
| }; |
| |
| // A class that repeatedly runs a function, which is supplied at construction, |
| // until it evaluates to true. |
| class SuccessWaiter { |
| public: |
| SuccessWaiter(const base::RepeatingCallback<bool()>& is_success, |
| const std::string& error_message) |
| : is_success_(is_success), error_message_(error_message) {} |
| ~SuccessWaiter() = default; |
| SuccessWaiter(const SuccessWaiter&) = delete; |
| SuccessWaiter& operator=(const SuccessWaiter&) = delete; |
| |
| void Wait() { |
| timer_.Start(FROM_HERE, base::Milliseconds(200), this, |
| &SuccessWaiter::OnTimer); |
| content::GetUIThreadTaskRunner({})->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&SuccessWaiter::MaybePrintErrorMessage, |
| weak_factory_.GetWeakPtr()), |
| base::Milliseconds(kPrintErrorMessageDelayMs)); |
| run_loop_.Run(); |
| ASSERT_TRUE(is_success_.Run()); |
| } |
| |
| void OnTimer() { |
| if (is_success_.Run()) { |
| timer_.Stop(); |
| run_loop_.Quit(); |
| } |
| } |
| |
| void MaybePrintErrorMessage() { |
| if (!timer_.IsRunning() || run_loop_.AnyQuitCalled() || is_success_.Run()) |
| return; |
| |
| LOG(ERROR) << "Still waiting for SuccessWaiter\n" |
| << "SuccessWaiter error message: " << error_message_ << "\n"; |
| } |
| |
| private: |
| base::RepeatingCallback<bool()> is_success_; |
| std::string error_message_; |
| base::RepeatingTimer timer_; |
| base::RunLoop run_loop_; |
| base::WeakPtrFactory<SuccessWaiter> weak_factory_{this}; |
| }; |
| |
| class CaretBoundsChangedWaiter : public ui::InputMethodObserver { |
| public: |
| explicit CaretBoundsChangedWaiter(ui::InputMethod* input_method) |
| : input_method_(input_method) { |
| input_method_->AddObserver(this); |
| } |
| CaretBoundsChangedWaiter(const CaretBoundsChangedWaiter&) = delete; |
| CaretBoundsChangedWaiter& operator=(const CaretBoundsChangedWaiter&) = delete; |
| ~CaretBoundsChangedWaiter() override { input_method_->RemoveObserver(this); } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| // ui::InputMethodObserver: |
| void OnFocus() override {} |
| void OnBlur() override {} |
| void OnTextInputStateChanged(const ui::TextInputClient* client) override {} |
| void OnInputMethodDestroyed(const ui::InputMethod* input_method) override {} |
| void OnCaretBoundsChanged(const ui::TextInputClient* client) override { |
| run_loop_.Quit(); |
| } |
| |
| ui::InputMethod* input_method_; |
| base::RunLoop run_loop_; |
| }; |
| |
| // Listens for changes to the clipboard. This class only allows `Wait()` to be |
| // called once. If you need to call `Wait()` multiple times, create multiple |
| // instances of this class. |
| class ClipboardChangedWaiter : public ui::ClipboardObserver { |
| public: |
| ClipboardChangedWaiter() { |
| ui::ClipboardMonitor::GetInstance()->AddObserver(this); |
| } |
| ClipboardChangedWaiter(const ClipboardChangedWaiter&) = delete; |
| ClipboardChangedWaiter& operator=(const ClipboardChangedWaiter&) = delete; |
| ~ClipboardChangedWaiter() override { |
| ui::ClipboardMonitor::GetInstance()->RemoveObserver(this); |
| } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| // ui::ClipboardObserver: |
| void OnClipboardDataChanged() override { run_loop_.Quit(); } |
| |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| class DictationTestBase |
| : public InProcessBrowserTest, |
| public ::testing::WithParamInterface<speech::SpeechRecognitionType> { |
| public: |
| DictationTestBase() : test_helper_(GetParam()) {} |
| ~DictationTestBase() override = default; |
| DictationTestBase(const DictationTestBase&) = delete; |
| DictationTestBase& operator=(const DictationTestBase&) = delete; |
| |
| protected: |
| // InProcessBrowserTest: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| std::vector<base::Feature> enabled_features = |
| test_helper_.GetEnabledFeatures(); |
| std::vector<base::Feature> disabled_features = |
| test_helper_.GetDisabledFeatures(); |
| scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| |
| InProcessBrowserTest::SetUpCommandLine(command_line); |
| } |
| |
| void SetUpOnMainThread() override { |
| InProcessBrowserTest::SetUpOnMainThread(); |
| test_helper_.SetUp(browser()->profile()); |
| |
| ASSERT_FALSE(AccessibilityManager::Get()->IsDictationEnabled()); |
| console_observer_ = std::make_unique<ExtensionConsoleErrorObserver>( |
| browser()->profile(), extension_misc::kAccessibilityCommonExtensionId); |
| browser()->profile()->GetPrefs()->SetBoolean( |
| ash::prefs::kDictationAcceleratorDialogHasBeenAccepted, true); |
| |
| extensions::ExtensionHostTestHelper host_helper( |
| browser()->profile(), extension_misc::kAccessibilityCommonExtensionId); |
| AccessibilityManager::Get()->SetDictationEnabled(true); |
| host_helper.WaitForHostCompletedFirstLoad(); |
| |
| aura::Window* root_window = Shell::Get()->GetPrimaryRootWindow(); |
| generator_ = std::make_unique<ui::test::EventGenerator>(root_window); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| GURL( |
| "data:text/html;charset=utf-8,<textarea id=textarea></textarea>"))); |
| // Put focus in the text box. |
| ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| nullptr, ui::KeyboardCode::VKEY_TAB, false, false, false, false))); |
| } |
| |
| void TearDownOnMainThread() override { |
| if (GetParam() == speech::SpeechRecognitionType::kNetwork) |
| content::SpeechRecognitionManager::SetManagerForTesting(nullptr); |
| |
| InProcessBrowserTest::TearDownOnMainThread(); |
| } |
| |
| // Routers to SpeechRecognitionTestHelper methods. |
| void WaitForRecognitionStarted() { |
| test_helper_.WaitForRecognitionStarted(); |
| // Dictation initializes FocusHandler when speech recognition starts. |
| // Several tests require FocusHandler logic, so wait for it to initialize |
| // before proceeding. |
| WaitForFocusHandler(); |
| } |
| |
| void WaitForRecognitionStopped() { test_helper_.WaitForRecognitionStopped(); } |
| |
| void SendInterimResultAndWait(const std::string& transcript) { |
| test_helper_.SendInterimResultAndWait(transcript); |
| } |
| |
| void SendFinalResultAndWait(const std::string& transcript) { |
| test_helper_.SendFinalResultAndWait(transcript); |
| } |
| |
| void SendErrorAndWait() { test_helper_.SendErrorAndWait(); } |
| |
| void SendFinalResultAndWaitForTextAreaValue(const std::string& result, |
| const std::string& value) { |
| // Ensure that the accessibility tree and the text area value are updated. |
| content::AccessibilityNotificationWaiter waiter( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| ui::kAXModeComplete, ax::mojom::Event::kValueChanged); |
| SendFinalResultAndWait(result); |
| // TODO(https://crbug.com/1333354): Investigate why this does not always |
| // return true. |
| ASSERT_TRUE(waiter.WaitForNotification()); |
| WaitForTextAreaValue(value); |
| } |
| |
| void SendFinalResultAndWaitForSelectionChanged(const std::string& result) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| content::AccessibilityNotificationWaiter selection_waiter( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| ui::kAXModeComplete, |
| ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED); |
| content::BoundingBoxUpdateWaiter bounding_box_waiter(web_contents); |
| SendFinalResultAndWait(result); |
| bounding_box_waiter.Wait(); |
| // TODO(https://crbug.com/1333354): Investigate why this does not always |
| // return true. |
| ASSERT_TRUE(selection_waiter.WaitForNotification()); |
| } |
| |
| void SendFinalResultAndWaitForCaretBoundsChanged(const std::string& result) { |
| content::AccessibilityNotificationWaiter selection_waiter( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| ui::kAXModeComplete, |
| ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED); |
| CaretBoundsChangedWaiter caret_waiter( |
| browser()->window()->GetNativeWindow()->GetHost()->GetInputMethod()); |
| SendFinalResultAndWait(result); |
| caret_waiter.Wait(); |
| // TODO(https://crbug.com/1333354): Investigate why this does not always |
| // return true. |
| ASSERT_TRUE(selection_waiter.WaitForNotification()); |
| } |
| |
| void SendFinalResultAndWaitForClipboardChanged(const std::string& result) { |
| ClipboardChangedWaiter waiter; |
| SendFinalResultAndWait(result); |
| waiter.Wait(); |
| } |
| |
| std::string GetTextAreaValue() { |
| std::string output; |
| std::string script = |
| "window.domAutomationController.send(" |
| "document.getElementById('textarea').value)"; |
| CHECK(ExecuteScriptAndExtractString( |
| browser()->tab_strip_model()->GetWebContentsAt(0), script, &output)); |
| return output; |
| } |
| |
| void WaitForTextAreaValue(const std::string& value) { |
| std::string error_message = "Still waiting for text area value: " + value; |
| SuccessWaiter(base::BindLambdaForTesting( |
| [&]() { return value == GetTextAreaValue(); }), |
| error_message) |
| .Wait(); |
| } |
| |
| void WaitForFocusHandler() { |
| std::string error_message = "Still waiting for FocusHandler"; |
| std::string script = R"( |
| if (accessibilityCommon.dictation_.focusHandler_.isReadyForTesting()) { |
| window.domAutomationController.send("ready"); |
| } else { |
| window.domAutomationController.send("not ready"); |
| } |
| )"; |
| SuccessWaiter( |
| base::BindLambdaForTesting([&]() { |
| std::string result = |
| extensions::browsertest_util::ExecuteScriptInBackgroundPage( |
| /*context=*/browser()->profile(), |
| /*extension_id=*/ |
| extension_misc::kAccessibilityCommonExtensionId, |
| /*script=*/script); |
| return result == "ready"; |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| void ToggleDictationWithKeystroke() { |
| ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| nullptr, ui::KeyboardCode::VKEY_D, false, false, false, true))); |
| } |
| |
| void InstallMockInputContextHandler() { |
| input_context_handler_ = std::make_unique<ui::MockIMEInputContextHandler>(); |
| ui::IMEBridge::Get()->SetInputContextHandler(input_context_handler_.get()); |
| } |
| |
| // Retrieves the number of times commit text is updated. |
| int GetCommitTextCallCount() { |
| EXPECT_TRUE(input_context_handler_); |
| return input_context_handler_->commit_text_call_count(); |
| } |
| |
| void WaitForCommitText(const std::u16string& value) { |
| std::string error_message = |
| base::UTF16ToUTF8(u"Still waiting for commit text: " + value); |
| EXPECT_TRUE(input_context_handler_); |
| SuccessWaiter(base::BindLambdaForTesting([&]() { |
| return value == input_context_handler_->last_commit_text(); |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| const base::flat_map<std::string, Dictation::LocaleData> |
| GetAllSupportedLocales() { |
| return Dictation::GetAllSupportedLocales(); |
| } |
| |
| private: |
| SpeechRecognitionTestHelper test_helper_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| std::unique_ptr<ui::MockIMEInputContextHandler> input_context_handler_; |
| std::unique_ptr<ui::test::EventGenerator> generator_; |
| std::unique_ptr<ExtensionConsoleErrorObserver> console_observer_; |
| }; |
| |
| class DictationTest : public DictationTestBase { |
| public: |
| DictationTest() = default; |
| ~DictationTest() override = default; |
| DictationTest(const DictationTest&) = delete; |
| DictationTest& operator=(const DictationTest&) = delete; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| GetActiveUserPrefs()->SetString(prefs::kAccessibilityDictationLocale, |
| "en-US"); |
| DictationTestBase::SetUpOnMainThread(); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Network, |
| DictationTest, |
| ::testing::Values(speech::SpeechRecognitionType::kNetwork)); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| OnDevice, |
| DictationTest, |
| ::testing::Values(speech::SpeechRecognitionType::kOnDevice)); |
| |
| // Tests the behavior of the GetAllSupportedLocales method, specifically how |
| // it sets locale data. |
| IN_PROC_BROWSER_TEST_P(DictationTest, GetAllSupportedLocales) { |
| auto locales = GetAllSupportedLocales(); |
| for (auto& it : locales) { |
| const std::string locale = it.first; |
| bool works_offline = it.second.works_offline; |
| bool installed = it.second.installed; |
| if (GetParam() == speech::SpeechRecognitionType::kOnDevice && |
| locale == speech::kUsEnglishLocale) { |
| // Currently, the only locale supported by SODA is en-US. It should work |
| // offline and be installed. |
| EXPECT_TRUE(works_offline); |
| EXPECT_TRUE(installed); |
| } else { |
| EXPECT_FALSE(works_offline); |
| EXPECT_FALSE(installed); |
| } |
| } |
| |
| if (GetParam() == speech::SpeechRecognitionType::kOnDevice) { |
| // Uninstall SODA and all language packs. |
| speech::SodaInstaller::GetInstance()->UninstallSodaForTesting(); |
| } else { |
| return; |
| } |
| |
| locales = GetAllSupportedLocales(); |
| for (auto& it : locales) { |
| const std::string locale = it.first; |
| bool works_offline = it.second.works_offline; |
| bool installed = it.second.installed; |
| if (locale == speech::kUsEnglishLocale) { |
| // en-US should be marked as "works offline", but it shouldn't be |
| // installed. |
| EXPECT_TRUE(works_offline); |
| EXPECT_FALSE(installed); |
| } else { |
| EXPECT_FALSE(works_offline); |
| EXPECT_FALSE(installed); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, StartsAndStopsRecognition) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, EntersFinalizedSpeech) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue(kFinalSpeechResult, |
| kFinalSpeechResult); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| // Tests that multiple finalized strings can be committed to the text area. |
| // Also ensures that spaces are added between finalized utterances. |
| IN_PROC_BROWSER_TEST_P(DictationTest, EntersMultipleFinalizedStrings) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("The rain in Spain", |
| "The rain in Spain"); |
| SendFinalResultAndWaitForTextAreaValue( |
| "falls mainly on the plain.", |
| "The rain in Spain falls mainly on the plain."); |
| SendFinalResultAndWaitForTextAreaValue( |
| "Vega is a star.", |
| "The rain in Spain falls mainly on the plain. Vega is a star."); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, OnlyAddSpaceWhenNecessary) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("The rain in Spain", |
| "The rain in Spain"); |
| // Artificially add a space to this utterance. Verify that only one space is |
| // added. |
| SendFinalResultAndWaitForTextAreaValue( |
| " falls mainly on the plain.", |
| "The rain in Spain falls mainly on the plain."); |
| // Artificially add a space to this utterance. Verify that only one space is |
| // added. |
| SendFinalResultAndWaitForTextAreaValue( |
| " Vega is a star.", |
| "The rain in Spain falls mainly on the plain. Vega is a star."); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, RecognitionEndsWhenInputFieldLosesFocus) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("Vega is a star", "Vega is a star"); |
| ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync( |
| nullptr, ui::KeyboardCode::VKEY_TAB, false, false, false, false))); |
| WaitForRecognitionStopped(); |
| EXPECT_EQ("Vega is a star", GetTextAreaValue()); |
| } |
| |
| // TODO(crbug.com/1352312): Flaky. |
| IN_PROC_BROWSER_TEST_P(DictationTest, |
| DISABLED_UserEndsDictationWhenChromeVoxEnabled) { |
| EnableChromeVox(); |
| EXPECT_TRUE(GetManager()->IsSpokenFeedbackEnabled()); |
| InstallMockInputContextHandler(); |
| |
| GetManager()->ToggleDictation(); |
| WaitForRecognitionStarted(); |
| SendInterimResultAndWait(kFinalSpeechResult); |
| GetManager()->ToggleDictation(); |
| WaitForRecognitionStopped(); |
| |
| WaitForCommitText(kFinalSpeechResult16); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, ChromeVoxSilencedWhenToggledOn) { |
| // Set up ChromeVox. |
| test::SpeechMonitor sm; |
| EXPECT_FALSE(GetManager()->IsSpokenFeedbackEnabled()); |
| extensions::ExtensionHostTestHelper host_helper( |
| browser()->profile(), extension_misc::kChromeVoxExtensionId); |
| EnableChromeVox(); |
| host_helper.WaitForHostCompletedFirstLoad(); |
| EXPECT_TRUE(GetManager()->IsSpokenFeedbackEnabled()); |
| |
| // Not yet forced to stop. |
| EXPECT_EQ(0, sm.stop_count()); |
| |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| |
| // Assert ChromeVox was asked to stop speaking at the toggle. |
| EXPECT_EQ(1, sm.stop_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, EntersInterimSpeechWhenToggledOff) { |
| InstallMockInputContextHandler(); |
| |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendInterimResultAndWait(kFirstSpeechResult); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| WaitForCommitText(kFirstSpeechResult16); |
| } |
| |
| // Tests that commit text is not updated if the user toggles dictation and no |
| // speech results are processed. |
| IN_PROC_BROWSER_TEST_P(DictationTest, UserEndsDictationBeforeSpeech) { |
| InstallMockInputContextHandler(); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| EXPECT_EQ(0, GetCommitTextCallCount()); |
| } |
| |
| // Ensures that the correct metrics are recorded when Dictation is toggled. |
| IN_PROC_BROWSER_TEST_P(DictationTest, Metrics) { |
| base::HistogramTester histogram_tester_; |
| bool on_device = GetParam() == speech::SpeechRecognitionType::kOnDevice; |
| const char* metric_name = on_device ? kOnDeviceListeningDurationMetric |
| : kNetworkListeningDurationMetric; |
| HistogramWaiter waiter(metric_name); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| waiter.Wait(); |
| content::FetchHistogramsFromChildProcesses(); |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| |
| // Ensure that we recorded the correct locale. |
| histogram_tester_.ExpectUniqueSample(/*name=*/kLocaleMetric, |
| /*sample=*/base::HashMetricName("en-US"), |
| /*expected_bucket_count=*/1); |
| // Ensure that we recorded the type of speech recognition and listening |
| // duration. |
| if (on_device) { |
| histogram_tester_.ExpectUniqueSample(/*name=*/kOnDeviceSpeechMetric, |
| /*sample=*/true, |
| /*expected_bucket_count=*/1); |
| ASSERT_EQ(1u, |
| histogram_tester_.GetAllSamples(kOnDeviceListeningDurationMetric) |
| .size()); |
| // Ensure there are no metrics for the other type of speech recognition. |
| ASSERT_EQ(0u, |
| histogram_tester_.GetAllSamples(kNetworkListeningDurationMetric) |
| .size()); |
| } else { |
| histogram_tester_.ExpectUniqueSample(/*name=*/kOnDeviceSpeechMetric, |
| /*sample=*/false, |
| /*expected_bucket_count=*/1); |
| ASSERT_EQ(1u, |
| histogram_tester_.GetAllSamples(kNetworkListeningDurationMetric) |
| .size()); |
| // Ensure there are no metrics for the other type of speech recognition. |
| ASSERT_EQ(0u, |
| histogram_tester_.GetAllSamples(kOnDeviceListeningDurationMetric) |
| .size()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, |
| DictationStopsWhenSystemTrayBecomesVisible) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SystemTrayTestApi::Create()->ShowBubble(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, NoExtraSpaceForPunctuation) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("Hello world", "Hello world"); |
| SendFinalResultAndWaitForTextAreaValue(".", "Hello world."); |
| SendFinalResultAndWaitForTextAreaValue("Goodnight", "Hello world. Goodnight"); |
| SendFinalResultAndWaitForTextAreaValue("!", "Hello world. Goodnight!"); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, StopListening) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWait("cancel"); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, SmartCapitalization) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("This", "This"); |
| SendFinalResultAndWaitForTextAreaValue("Is", "This is"); |
| SendFinalResultAndWaitForTextAreaValue("a test.", "This is a test."); |
| SendFinalResultAndWaitForTextAreaValue("you passed!", |
| "This is a test. You passed!"); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationTest, SmartCapitalizationWithComma) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("Hello,", "Hello,"); |
| SendFinalResultAndWaitForTextAreaValue("world", "Hello, world"); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| // Tests the behavior of Dictation in other languages. |
| class DictationI18NTest : public DictationTestBase { |
| public: |
| DictationI18NTest() = default; |
| ~DictationI18NTest() override = default; |
| DictationI18NTest(const DictationI18NTest&) = delete; |
| DictationI18NTest& operator=(const DictationI18NTest&) = delete; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| GetActiveUserPrefs()->SetString(prefs::kAccessibilityDictationLocale, |
| "ja-JP"); |
| DictationTestBase::SetUpOnMainThread(); |
| } |
| }; |
| |
| // On-device speech recognition is currently limited to en-US, so |
| // DictationI18NTest should use network speech recognition only. |
| INSTANTIATE_TEST_SUITE_P( |
| Network, |
| DictationI18NTest, |
| ::testing::Values(speech::SpeechRecognitionType::kNetwork)); |
| |
| IN_PROC_BROWSER_TEST_P(DictationI18NTest, NoSmartSpacingOrCapitalization) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| SendFinalResultAndWaitForTextAreaValue("this", "this"); |
| SendFinalResultAndWaitForTextAreaValue(" Is", "this Is"); |
| SendFinalResultAndWaitForTextAreaValue("a test.", "this Isa test."); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| } |
| |
| class DictationCommandsTest : public DictationTest { |
| protected: |
| DictationCommandsTest() {} |
| ~DictationCommandsTest() override = default; |
| DictationCommandsTest(const DictationCommandsTest&) = delete; |
| DictationCommandsTest& operator=(const DictationCommandsTest&) = delete; |
| |
| void SetUpOnMainThread() override { |
| DictationTest::SetUpOnMainThread(); |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| } |
| |
| void TearDownOnMainThread() override { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| DictationTest::TearDownOnMainThread(); |
| } |
| |
| std::string GetClipboardText() { |
| std::u16string text; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::ClipboardBuffer::kCopyPaste, /*data_dst=*/nullptr, &text); |
| return base::UTF16ToUTF8(text); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Network, |
| DictationCommandsTest, |
| ::testing::Values(speech::SpeechRecognitionType::kNetwork)); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| OnDevice, |
| DictationCommandsTest, |
| ::testing::Values(speech::SpeechRecognitionType::kOnDevice)); |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, TypesCommands) { |
| std::string expected_text = ""; |
| int i = 0; |
| for (const char* command : kEnglishDictationCommands) { |
| std::string type_command = "type "; |
| if (i == 0) { |
| expected_text += command; |
| expected_text[0] = base::ToUpperASCII(expected_text[0]); |
| } else { |
| expected_text += " "; |
| expected_text += command; |
| } |
| SendFinalResultAndWaitForTextAreaValue(type_command + command, |
| expected_text); |
| ++i; |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, TypesNonCommands) { |
| // The phrase should be entered without the word "type". |
| SendFinalResultAndWaitForTextAreaValue("Type this is a test", |
| "This is a test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeleteCharacter) { |
| SendFinalResultAndWaitForTextAreaValue("Vega", "Vega"); |
| // Capitalization and whitespace shouldn't matter. |
| SendFinalResultAndWaitForTextAreaValue(" Delete", "Veg"); |
| SendFinalResultAndWaitForTextAreaValue("delete", "Ve"); |
| SendFinalResultAndWaitForTextAreaValue(" delete ", "V"); |
| SendFinalResultAndWaitForTextAreaValue("DELETE", ""); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, MoveByCharacter) { |
| SendFinalResultAndWaitForTextAreaValue("Lyra", "Lyra"); |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the Previous character"); |
| // White space is added to the text on the left of the text caret, but not |
| // to the right of the text caret. |
| SendFinalResultAndWaitForTextAreaValue("inserted", "Lyr inserted a"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move TO the next character "); |
| SendFinalResultAndWaitForTextAreaValue("is a constellation", |
| "Lyr inserted a is a constellation"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, NewLineAndMoveByLine) { |
| SendFinalResultAndWaitForTextAreaValue("Line 1", "Line 1"); |
| SendFinalResultAndWaitForTextAreaValue("new line", "Line 1\n"); |
| SendFinalResultAndWaitForTextAreaValue("line 2", "Line 1\nline 2"); |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the previous line "); |
| SendFinalResultAndWaitForTextAreaValue("up", "Line 1 up\nline 2"); |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the next line"); |
| SendFinalResultAndWaitForTextAreaValue("down", "Line 1 up\nline 2 down"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, UndoAndRedo) { |
| SendFinalResultAndWaitForTextAreaValue("The constellation", |
| "The constellation"); |
| SendFinalResultAndWaitForTextAreaValue(" Myra", "The constellation Myra"); |
| SendFinalResultAndWaitForTextAreaValue("undo", "The constellation"); |
| SendFinalResultAndWaitForTextAreaValue(" Lyra", "The constellation Lyra"); |
| SendFinalResultAndWaitForTextAreaValue("undo", "The constellation"); |
| SendFinalResultAndWaitForTextAreaValue("redo", "The constellation Lyra"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, SelectAllAndUnselect) { |
| SendFinalResultAndWaitForTextAreaValue("Vega is the brightest star in Lyra", |
| "Vega is the brightest star in Lyra"); |
| SendFinalResultAndWaitForSelectionChanged("Select all"); |
| SendFinalResultAndWaitForTextAreaValue("delete", ""); |
| SendFinalResultAndWaitForTextAreaValue( |
| "Vega is the fifth brightest star in the sky", |
| "Vega is the fifth brightest star in the sky"); |
| SendFinalResultAndWaitForSelectionChanged("Select all"); |
| SendFinalResultAndWaitForSelectionChanged("Unselect"); |
| SendFinalResultAndWaitForTextAreaValue( |
| "!", "Vega is the fifth brightest star in the sky!"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, CutCopyPaste) { |
| SendFinalResultAndWaitForTextAreaValue("Star", "Star"); |
| SendFinalResultAndWaitForSelectionChanged("Select all"); |
| SendFinalResultAndWaitForClipboardChanged("Copy"); |
| EXPECT_EQ("Star", GetClipboardText()); |
| SendFinalResultAndWaitForSelectionChanged("unselect"); |
| SendFinalResultAndWaitForTextAreaValue("paste", "StarStar"); |
| SendFinalResultAndWaitForSelectionChanged("select ALL "); |
| SendFinalResultAndWaitForClipboardChanged("cut"); |
| EXPECT_EQ("StarStar", GetClipboardText()); |
| WaitForTextAreaValue(""); |
| SendFinalResultAndWaitForTextAreaValue(" PaStE ", "StarStar"); |
| } |
| |
| // Ensures that a metric is recorded when a macro succeeds. |
| // TODO(crbug.com/1288964): Add a test to ensure that a metric is recorded when |
| // a macro fails. |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, MacroSucceededMetric) { |
| base::HistogramTester histogram_tester_; |
| SendFinalResultAndWaitForTextAreaValue("Vega is the brightest star in Lyra", |
| "Vega is the brightest star in Lyra"); |
| histogram_tester_.ExpectUniqueSample(/*name=*/kMacroSucceededMetric, |
| /*sample=*/kInputTextViewMetricValue, |
| /*expected_bucket_count=*/1); |
| histogram_tester_.ExpectUniqueSample(/*name=*/kMacroFailedMetric, |
| /*sample=*/kInputTextViewMetricValue, |
| /*expected_bucket_count=*/0); |
| histogram_tester_.ExpectUniqueSample(/*name=*/kMacroRecognizedMetric, |
| /*sample=*/kInputTextViewMetricValue, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, Help) { |
| SendFinalResultAndWait("help"); |
| |
| // Wait for the help URL to load. |
| SuccessWaiter( |
| base::BindLambdaForTesting([&]() { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| return web_contents->GetVisibleURL() == |
| "https://support.google.com/chromebook?p=text_dictation_m100"; |
| }), |
| "Still waiting for help URL to load") |
| .Wait(); |
| |
| // Opening a new tab with the help center article toggles Dictation off. |
| WaitForRecognitionStopped(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevWordSimple) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test", "This is a test"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous word", |
| "This is a "); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevWordExtraSpace) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test ", "This is a test "); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous word", |
| "This is a "); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevWordNewLine) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test\n\n", |
| "This is a test\n\n"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous word", |
| "This is a test\n"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevWordPunctuation) { |
| SendFinalResultAndWaitForTextAreaValue("This.is.a.test. ", |
| "This.is.a.test. "); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous word", |
| "This.is.a.test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevWordMiddleOfWord) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test.", "This is a test."); |
| // Move the text caret into the middle of the word "test". |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the Previous character"); |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the Previous character"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous word", |
| "This is a t."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevSentSimple) { |
| SendFinalResultAndWaitForTextAreaValue("Hello, world.", "Hello, world."); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", ""); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevSentWhiteSpace) { |
| SendFinalResultAndWaitForTextAreaValue(" \nHello, world.\n ", |
| " \nHello, world.\n "); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", ""); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevSentPunctuation) { |
| SendFinalResultAndWaitForTextAreaValue( |
| "Hello, world! Good afternoon; good evening? Goodnight, world.", |
| "Hello, world! Good afternoon; good evening? Goodnight, world."); |
| SendFinalResultAndWaitForTextAreaValue( |
| "delete the previous sentence", |
| "Hello, world! Good afternoon; good evening?"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", |
| "Hello, world! Good afternoon;"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", |
| "Hello, world!"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevSentTwoSentences) { |
| SendFinalResultAndWaitForTextAreaValue("Hello, world. Goodnight, world.", |
| "Hello, world. Goodnight, world."); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", |
| "Hello, world."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, DeletePrevSentMiddleOfSentence) { |
| SendFinalResultAndWaitForTextAreaValue("Hello, world. Goodnight, world.", |
| "Hello, world. Goodnight, world."); |
| // Move the text caret into the middle of the second sentence. |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the Previous character"); |
| SendFinalResultAndWaitForCaretBoundsChanged("Move to the Previous character"); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", |
| "Hello, world.d."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, MoveByWord) { |
| SendFinalResultAndWaitForTextAreaValue("This is a quiz", "This is a quiz"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the previous word"); |
| SendFinalResultAndWaitForTextAreaValue("pop ", "This is a pop quiz"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the next word"); |
| SendFinalResultAndWaitForTextAreaValue("folks!", "This is a pop quiz folks!"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, SmartDeletePhraseSimple) { |
| SendFinalResultAndWaitForTextAreaValue("This is a difficult test", |
| "This is a difficult test"); |
| SendFinalResultAndWaitForTextAreaValue("delete difficult", "This is a test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, |
| SmartDeletePhraseCaseInsensitive) { |
| SendFinalResultAndWaitForTextAreaValue("This is a DIFFICULT test", |
| "This is a DIFFICULT test"); |
| SendFinalResultAndWaitForTextAreaValue("delete difficult", "This is a test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, |
| SmartDeletePhraseDuplicateMatches) { |
| SendFinalResultAndWaitForTextAreaValue("The cow jumped over the moon.", |
| "The cow jumped over the moon."); |
| // Deletes the right-most occurrence of "the". |
| SendFinalResultAndWaitForTextAreaValue("delete the", |
| "The cow jumped over moon."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, |
| SmartDeletePhraseDeletesLeftOfCaret) { |
| SendFinalResultAndWaitForTextAreaValue("The cow jumped over the moon.", |
| "The cow jumped over the moon."); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the previous word"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the previous word"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the previous word"); |
| SendFinalResultAndWaitForTextAreaValue("delete the", |
| "cow jumped over the moon."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, |
| SmartDeletePhraseDeletesAtWordBoundaries) { |
| SendFinalResultAndWaitForTextAreaValue("A square is also a rectangle.", |
| "A square is also a rectangle."); |
| // Deletes the first word "a", not the first character "a". |
| SendFinalResultAndWaitForTextAreaValue("delete a", |
| "A square is also rectangle."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, SmartReplacePhrase) { |
| SendFinalResultAndWaitForTextAreaValue("This is a difficult test.", |
| "This is a difficult test."); |
| SendFinalResultAndWaitForTextAreaValue("replace difficult with simple", |
| "This is a simple test."); |
| SendFinalResultAndWaitForTextAreaValue("replace is with isn't", |
| "This isn't a simple test."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, SmartInsertBefore) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test.", "This is a test."); |
| SendFinalResultAndWaitForTextAreaValue("insert simple before test", |
| "This is a simple test."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, SmartSelectBetween) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test.", "This is a test."); |
| SendFinalResultAndWaitForSelectionChanged("select from this to test"); |
| SendFinalResultAndWaitForTextAreaValue("Hello world", "Hello world."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, MoveBySentence) { |
| SendFinalResultAndWaitForTextAreaValue("Hello world! Goodnight world?", |
| "Hello world! Goodnight world?"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the previous sentence"); |
| SendFinalResultAndWaitForTextAreaValue( |
| "Good evening.", "Hello world! Good evening. Goodnight world?"); |
| SendFinalResultAndWaitForCaretBoundsChanged("move to the next sentence"); |
| SendFinalResultAndWaitForTextAreaValue( |
| "Time for a midnight snack", |
| "Hello world! Good evening. Goodnight world? Time for a midnight snack"); |
| } |
| |
| // CursorPosition... tests verify the new cursor position after a command is |
| // performed. The new cursor position is verified by inserting text after the |
| // command under test is performed. |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, CursorPositionDeleteSentence) { |
| SendFinalResultAndWaitForTextAreaValue("First. Second.", "First. Second."); |
| SendFinalResultAndWaitForTextAreaValue("delete the previous sentence", |
| "First."); |
| SendFinalResultAndWaitForTextAreaValue("Third.", "First. Third."); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, CursorPositionSmartDeletePhrase) { |
| SendFinalResultAndWaitForTextAreaValue("This is a difficult test", |
| "This is a difficult test"); |
| SendFinalResultAndWaitForCaretBoundsChanged("delete difficult"); |
| SendFinalResultAndWaitForTextAreaValue("simple", "This is a simple test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, |
| CursorPositionSmartReplacePhrase) { |
| SendFinalResultAndWaitForTextAreaValue("This is a difficult test", |
| "This is a difficult test"); |
| SendFinalResultAndWaitForCaretBoundsChanged("replace difficult with simple"); |
| SendFinalResultAndWaitForTextAreaValue("biology", |
| "This is a simple biology test"); |
| SendFinalResultAndWaitForTextAreaValue( |
| "and chemistry", "This is a simple biology and chemistry test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationCommandsTest, CursorPositionSmartInsertBefore) { |
| SendFinalResultAndWaitForTextAreaValue("This is a test", "This is a test"); |
| SendFinalResultAndWaitForCaretBoundsChanged("insert simple before test"); |
| SendFinalResultAndWaitForTextAreaValue("biology", |
| "This is a simple biology test"); |
| } |
| |
| // Tests the behavior of the Dictation bubble UI. |
| class DictationUITest : public DictationTest { |
| protected: |
| DictationUITest() = default; |
| ~DictationUITest() override = default; |
| DictationUITest(const DictationUITest&) = delete; |
| DictationUITest& operator=(const DictationUITest&) = delete; |
| |
| void SetUpOnMainThread() override { |
| DictationTest::SetUpOnMainThread(); |
| dictation_bubble_test_helper_ = |
| std::make_unique<DictationBubbleTestHelper>(); |
| } |
| |
| void WaitForProperties( |
| bool visible, |
| DictationBubbleIconType icon, |
| const absl::optional<std::u16string>& text, |
| const absl::optional<std::vector<std::u16string>>& hints) { |
| WaitForVisibility(visible); |
| WaitForVisibleIcon(icon); |
| if (text.has_value()) |
| WaitForVisibleText(text.value()); |
| if (hints.has_value()) |
| WaitForVisibleHints(hints.value()); |
| } |
| |
| private: |
| void WaitForVisibility(bool visible) { |
| std::string error_message = "Still waiting for UI visibility: "; |
| error_message += visible ? "true" : "false"; |
| SuccessWaiter(base::BindLambdaForTesting([&]() { |
| return dictation_bubble_test_helper_->IsVisible() == |
| visible; |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| void WaitForVisibleIcon(DictationBubbleIconType icon) { |
| std::string error_message = "Still waiting for UI icon: " + ToString(icon); |
| SuccessWaiter(base::BindLambdaForTesting([&]() { |
| return dictation_bubble_test_helper_->GetVisibleIcon() == |
| icon; |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| void WaitForVisibleText(const std::u16string& text) { |
| std::string error_message = |
| "Still waiting for UI text: " + base::UTF16ToUTF8(text); |
| SuccessWaiter(base::BindLambdaForTesting([&]() { |
| return dictation_bubble_test_helper_->GetText() == text; |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| void WaitForVisibleHints(const std::vector<std::u16string>& hints) { |
| std::string error_message = base::UTF16ToUTF8( |
| u"Still waiting for UI hints: " + base::JoinString(hints, u",")); |
| SuccessWaiter( |
| base::BindLambdaForTesting([&]() { |
| return dictation_bubble_test_helper_->HasVisibleHints(hints); |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| std::unique_ptr<DictationBubbleTestHelper> dictation_bubble_test_helper_; |
| }; |
| |
| // Consistently failing on Linux ChromiumOS MSan (https://crbug.com/1302688). |
| #if defined(MEMORY_SANITIZER) |
| #define MAYBE_ShownWhenSpeechRecognitionStarts \ |
| DISABLED_ShownWhenSpeechRecognitionStarts |
| #define MAYBE_DisplaysInterimSpeechResults DISABLED_DisplaysInterimSpeechResults |
| #define MAYBE_DisplaysMacroSuccess DISABLED_DisplaysMacroSuccess |
| #define MAYBE_ResetsToStandbyModeAfterFinalSpeechResult \ |
| DISABLED_ResetsToStandbyModeAfterFinalSpeechResult |
| #define MAYBE_DisplaysMacroSuccess DISABLED_DisplaysMacroSuccess |
| #define MAYBE_HiddenWhenDictationDeactivates \ |
| DISABLED_HiddenWhenDictationDeactivates |
| #define MAYBE_StandbyHints DISABLED_StandbyHints |
| #define MAYBE_HintsShownWhenTextCommitted DISABLED_HintsShownWhenTextCommitted |
| #define MAYBE_HintsShownAfterTextSelected DISABLED_HintsShownAfterTextSelected |
| #define MAYBE_HintsShownAfterCommandExecuted \ |
| DISABLED_HintsShownAfterCommandExecuted |
| #else |
| #define MAYBE_ShownWhenSpeechRecognitionStarts ShownWhenSpeechRecognitionStarts |
| #define MAYBE_DisplaysInterimSpeechResults DisplaysInterimSpeechResults |
| #define MAYBE_DisplaysMacroSuccess DisplaysMacroSuccess |
| #define MAYBE_ResetsToStandbyModeAfterFinalSpeechResult \ |
| ResetsToStandbyModeAfterFinalSpeechResult |
| #define MAYBE_DisplaysMacroSuccess DisplaysMacroSuccess |
| #define MAYBE_HiddenWhenDictationDeactivates HiddenWhenDictationDeactivates |
| #define MAYBE_StandbyHints StandbyHints |
| #define MAYBE_HintsShownWhenTextCommitted HintsShownWhenTextCommitted |
| #define MAYBE_HintsShownAfterTextSelected HintsShownAfterTextSelected |
| #define MAYBE_HintsShownAfterCommandExecuted HintsShownAfterCommandExecuted |
| #endif |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Network, |
| DictationUITest, |
| ::testing::Values(speech::SpeechRecognitionType::kNetwork)); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| OnDevice, |
| DictationUITest, |
| ::testing::Values(speech::SpeechRecognitionType::kOnDevice)); |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, |
| MAYBE_ShownWhenSpeechRecognitionStarts) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_DisplaysInterimSpeechResults) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| // Send an interim speech result. |
| SendInterimResultAndWait("Testing"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kHidden, |
| /*text=*/u"Testing", |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_DisplaysMacroSuccess) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| // Perform a command. |
| SendFinalResultAndWait("Select all"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kMacroSuccess, |
| /*text=*/u"Select all", |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| // UI should return to standby mode after a timeout. |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/std::u16string(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, |
| MAYBE_ResetsToStandbyModeAfterFinalSpeechResult) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| // Send an interim speech result. |
| SendInterimResultAndWait("Testing"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kHidden, |
| /*text=*/u"Testing", |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| // Send a final speech result. UI should return to standby mode. |
| SendFinalResultAndWait("Testing 123"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/std::u16string(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_HiddenWhenDictationDeactivates) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| // The UI should be hidden when Dictation deactivates. |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStopped(); |
| WaitForProperties(/*visible=*/false, |
| /*icon=*/DictationBubbleIconType::kHidden, |
| /*text=*/std::u16string(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_StandbyHints) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| // Hints should show up after a few seconds without speech. |
| WaitForProperties( |
| /*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/std::vector<std::u16string>{kTrySaying, kType, kHelp}); |
| } |
| |
| // Ensures that Search + D can be used to toggle Dictation when ChromeVox is |
| // active. Also verifies that ChromeVox announces hints when they are shown in |
| // the Dictation UI. |
| IN_PROC_BROWSER_TEST_P(DictationUITest, ChromeVoxAnnouncesHints) { |
| // Setup ChromeVox first. |
| test::SpeechMonitor sm; |
| EXPECT_FALSE(GetManager()->IsSpokenFeedbackEnabled()); |
| extensions::ExtensionHostTestHelper host_helper( |
| browser()->profile(), extension_misc::kChromeVoxExtensionId); |
| EnableChromeVox(); |
| host_helper.WaitForHostCompletedFirstLoad(); |
| EXPECT_TRUE(GetManager()->IsSpokenFeedbackEnabled()); |
| |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| |
| // Hints should show up after a few seconds without speech. |
| WaitForProperties( |
| /*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/std::vector<std::u16string>{kTrySaying, kType, kHelp}); |
| |
| // Assert speech from ChromeVox. |
| sm.ExpectSpeechPattern("Try saying*Type*Help*"); |
| sm.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_HintsShownWhenTextCommitted) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| |
| // Send a final speech result. UI should return to standby mode. |
| SendFinalResultAndWait("Testing"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/std::u16string(), |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| |
| // Hints should show up after a few seconds without speech. |
| WaitForProperties( |
| /*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/ |
| std::vector<std::u16string>{kTrySaying, kUndo, kDelete, kSelectAll, |
| kHelp}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_HintsShownAfterTextSelected) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| |
| // Perform a select all command. |
| SendFinalResultAndWaitForTextAreaValue("Vega is the brightest star in Lyra", |
| "Vega is the brightest star in Lyra"); |
| SendFinalResultAndWaitForSelectionChanged("Select all"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kMacroSuccess, |
| /*text=*/u"Select all", |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| |
| // UI should return to standby mode with hints after a few seconds without |
| // speech. |
| WaitForProperties( |
| /*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/ |
| std::vector<std::u16string>{kTrySaying, kUnselect, kCopy, kDelete, |
| kHelp}); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(DictationUITest, MAYBE_HintsShownAfterCommandExecuted) { |
| ToggleDictationWithKeystroke(); |
| WaitForRecognitionStarted(); |
| |
| // Perform a command. |
| SendFinalResultAndWait("Move to the previous character"); |
| WaitForProperties(/*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kMacroSuccess, |
| /*text=*/u"Move to the previous character", |
| /*hints=*/absl::optional<std::vector<std::u16string>>()); |
| |
| // UI should return to standby mode with hints after a few seconds without |
| // speech. |
| WaitForProperties( |
| /*visible=*/true, |
| /*icon=*/DictationBubbleIconType::kStandby, |
| /*text=*/absl::optional<std::u16string>(), |
| /*hints=*/std::vector<std::u16string>{kTrySaying, kUndo, kHelp}); |
| } |
| |
| // Tests behavior of Dictation macros that haven't been hooked up to the |
| // speech parser. |
| class DictationHiddenMacrosTest : public DictationTest { |
| protected: |
| DictationHiddenMacrosTest() = default; |
| ~DictationHiddenMacrosTest() = default; |
| DictationHiddenMacrosTest(const DictationHiddenMacrosTest&) = delete; |
| DictationHiddenMacrosTest& operator=(const DictationHiddenMacrosTest&) = |
| delete; |
| |
| void RunHiddenMacro(int macro) { |
| std::string script = base::StringPrintf(R"( |
| accessibilityCommon.dictation_.runHiddenMacroForTesting(%d); |
| window.domAutomationController.send("done"); |
| )", |
| macro); |
| ExecuteAccessibilityCommonScript(script); |
| } |
| |
| void RunHiddenMacroWithStringArg(int macro, const std::string& arg) { |
| std::string script = base::StringPrintf(R"( |
| accessibilityCommon.dictation_. |
| runHiddenMacroWithStringArgForTesting(%d, "%s"); |
| window.domAutomationController.send("done"); |
| )", |
| macro, arg.c_str()); |
| |
| ExecuteAccessibilityCommonScript(script); |
| } |
| |
| void RunHiddenMacroWithTwoStringArgs(int macro, |
| const std::string& arg1, |
| const std::string& arg2) { |
| std::string script = base::StringPrintf(R"( |
| accessibilityCommon.dictation_. |
| runHiddenMacroWithTwoStringArgsForTesting(%d, "%s", "%s"); |
| window.domAutomationController.send("done"); |
| )", |
| macro, arg1.c_str(), arg2.c_str()); |
| |
| ExecuteAccessibilityCommonScript(script); |
| } |
| |
| void RunMacroAndWaitForCaretBoundsChanged(int macro) { |
| content::AccessibilityNotificationWaiter selection_waiter( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| ui::kAXModeComplete, |
| ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED); |
| CaretBoundsChangedWaiter caret_waiter( |
| browser()->window()->GetNativeWindow()->GetHost()->GetInputMethod()); |
| RunHiddenMacro(macro); |
| caret_waiter.Wait(); |
| ASSERT_TRUE(selection_waiter.WaitForNotification()); |
| } |
| |
| void RunSmartSelectMacroAndWaitForSelectionChanged( |
| const std::string& start_phrase, |
| const std::string& end_phrase) { |
| content::AccessibilityNotificationWaiter selection_waiter( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| ui::kAXModeComplete, |
| ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED); |
| content::BoundingBoxUpdateWaiter bounding_box_waiter( |
| browser()->tab_strip_model()->GetActiveWebContents()); |
| RunHiddenMacroWithTwoStringArgs(/* SMART_SELECT_BTWN_INCL */ 24, |
| start_phrase, end_phrase); |
| bounding_box_waiter.Wait(); |
| ASSERT_TRUE(selection_waiter.WaitForNotification()); |
| } |
| |
| private: |
| void ExecuteAccessibilityCommonScript(const std::string& script) { |
| extensions::browsertest_util::ExecuteScriptInBackgroundPage( |
| /*context=*/browser()->profile(), |
| /*extension_id=*/extension_misc::kAccessibilityCommonExtensionId, |
| /*script=*/script); |
| } |
| }; |
| |
| // Add tests for hidden macros below. |
| |
| // Tests behavior of Dictation and installation of Pumpkin. |
| class DictationPumpkinInstallTest : public DictationTest { |
| protected: |
| DictationPumpkinInstallTest() = default; |
| ~DictationPumpkinInstallTest() = default; |
| DictationPumpkinInstallTest(const DictationPumpkinInstallTest&) = delete; |
| DictationPumpkinInstallTest& operator=(const DictationPumpkinInstallTest&) = |
| delete; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| DictationTest::SetUpCommandLine(command_line); |
| scoped_feature_list_.InitAndEnableFeature( |
| ::features::kExperimentalAccessibilityDictationWithPumpkin); |
| } |
| |
| void WaitForInstallToSucceed() { |
| std::string error_message = "Waiting for Pumpkin installation to succeed"; |
| SuccessWaiter(base::BindLambdaForTesting([&]() { |
| return AccessibilityManager::Get() |
| ->is_pumpkin_installed_for_testing(); |
| }), |
| error_message) |
| .Wait(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Network, |
| DictationPumpkinInstallTest, |
| ::testing::Values(speech::SpeechRecognitionType::kNetwork)); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| OnDevice, |
| DictationPumpkinInstallTest, |
| ::testing::Values(speech::SpeechRecognitionType::kOnDevice)); |
| |
| IN_PROC_BROWSER_TEST_P(DictationPumpkinInstallTest, WaitForInstall) { |
| // Dictation will request a Pumpkin install when it starts up. Wait for |
| // the install to succeed. |
| WaitForInstallToSucceed(); |
| } |
| |
| // TODO(crbug.com/1264544): Test looking at gn args has pumpkin and does |
| // repeats. |
| |
| } // namespace ash |