0

Reland: Dictation: Show toast when toggling Dictation fails

The original change was reverted because of failures on MSAN try
bots:
https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20ChromiumOS%20MSan%20Tests/37926/overview

The test passes consistently using many parallel jobs on a local
linux-chromeos-rel build, but I wasn't able to figure out a way to
stabilize the test for MSAN. I will disable the test on MSAN for now,
but added a TODO to address this before launching the new behavior.

Original commit description:

This change implements the behavior described in slide 7 of the UX
mocks [1]. When Dictation fails to toggle because there is no focused
text field, we now show a toast that explains why Dictation failed.
To do this we:

1. Added accessibilityPrivate.showToast(), so that the dictation
extension can request the toast to be shown.
2. Added AccessibilityNotificationController, which manages showing
the toast.

[1] https://docs.google.com/presentation/d/1mRnYcRpyzbY-EjzptCP3KRB5I7v_rM9Kl2zqhhQVZUY/edit#slide=id.g24cce1f4158_0_12

Bug: b:259352600
Change-Id: I3aac7437253aeb8423757038c763aa7ec10f7ccb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4802964
Reviewed-by: Mark Schillaci <mschillaci@google.com>
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1186678}
This commit is contained in:
Akihiro Ota
2023-08-22 18:20:25 +00:00
committed by Chromium LUCI CQ
parent 19dcafbc47
commit e145b6bd3e
28 changed files with 353 additions and 19 deletions

@@ -79,6 +79,8 @@ component("ash") {
"accessibility/accessibility_delegate.h", "accessibility/accessibility_delegate.h",
"accessibility/accessibility_event_handler_manager.cc", "accessibility/accessibility_event_handler_manager.cc",
"accessibility/accessibility_event_handler_manager.h", "accessibility/accessibility_event_handler_manager.h",
"accessibility/accessibility_notification_controller.cc",
"accessibility/accessibility_notification_controller.h",
"accessibility/accessibility_observer.h", "accessibility/accessibility_observer.h",
"accessibility/autoclick/autoclick_controller.cc", "accessibility/autoclick/autoclick_controller.cc",
"accessibility/autoclick/autoclick_controller.h", "accessibility/autoclick/autoclick_controller.h",

@@ -12,6 +12,7 @@
#include "ash/accelerators/accelerator_controller_impl.h" #include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/accessibility/a11y_feature_type.h" #include "ash/accessibility/a11y_feature_type.h"
#include "ash/accessibility/accessibility_notification_controller.h"
#include "ash/accessibility/accessibility_observer.h" #include "ash/accessibility/accessibility_observer.h"
#include "ash/accessibility/autoclick/autoclick_controller.h" #include "ash/accessibility/autoclick/autoclick_controller.h"
#include "ash/accessibility/dictation_nudge_controller.h" #include "ash/accessibility/dictation_nudge_controller.h"
@@ -969,10 +970,14 @@ AccessibilityControllerImpl::AccessibilityControllerImpl()
Shell::Get()->session_controller()->AddObserver(this); Shell::Get()->session_controller()->AddObserver(this);
Shell::Get()->tablet_mode_controller()->AddObserver(this); Shell::Get()->tablet_mode_controller()->AddObserver(this);
CreateAccessibilityFeatures(); CreateAccessibilityFeatures();
accessibility_notification_controller_ =
std::make_unique<AccessibilityNotificationController>();
} }
AccessibilityControllerImpl::~AccessibilityControllerImpl() { AccessibilityControllerImpl::~AccessibilityControllerImpl() {
floating_menu_controller_.reset(); floating_menu_controller_.reset();
accessibility_notification_controller_.reset();
} }
void AccessibilityControllerImpl::CreateAccessibilityFeatures() { void AccessibilityControllerImpl::CreateAccessibilityFeatures() {
@@ -1938,6 +1943,8 @@ void AccessibilityControllerImpl::OnDictationKeyboardDialogDismissed() {
void AccessibilityControllerImpl::ShowDictationLanguageUpgradedNudge( void AccessibilityControllerImpl::ShowDictationLanguageUpgradedNudge(
const std::string& dictation_locale, const std::string& dictation_locale,
const std::string& application_locale) { const std::string& application_locale) {
// TODO(b:259352600): Move dictation_nudge_controller_ into
// accessibility_notification_controller.
dictation_nudge_controller_ = std::make_unique<DictationNudgeController>( dictation_nudge_controller_ = std::make_unique<DictationNudgeController>(
dictation_locale, application_locale); dictation_locale, application_locale);
dictation_nudge_controller_->ShowNudge(); dictation_nudge_controller_->ShowNudge();
@@ -2925,4 +2932,14 @@ AccessibilityControllerImpl::GetDictationBubbleControllerForTest() {
return dictation_bubble_controller_.get(); return dictation_bubble_controller_.get();
} }
void AccessibilityControllerImpl::ShowToast(AccessibilityToastType type) {
accessibility_notification_controller_->ShowToast(type);
}
void AccessibilityControllerImpl::AddShowToastCallbackForTesting(
base::RepeatingClosure callback) {
accessibility_notification_controller_->AddShowToastCallbackForTesting(
std::move(callback));
}
} // namespace ash } // namespace ash

@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include "ash/accessibility/a11y_feature_type.h" #include "ash/accessibility/a11y_feature_type.h"
#include "ash/accessibility/accessibility_notification_controller.h"
#include "ash/ash_export.h" #include "ash/ash_export.h"
#include "ash/constants/ash_constants.h" #include "ash/constants/ash_constants.h"
#include "ash/public/cpp/accessibility_controller.h" #include "ash/public/cpp/accessibility_controller.h"
@@ -471,6 +472,7 @@ class ASH_EXPORT AccessibilityControllerImpl : public AccessibilityController,
const absl::optional<std::vector<DictationBubbleHintType>>& hints) const absl::optional<std::vector<DictationBubbleHintType>>& hints)
override; override;
void SilenceSpokenFeedback() override; void SilenceSpokenFeedback() override;
void ShowToast(AccessibilityToastType type) override;
// A confirmation dialog will be shown the first time an accessibility feature // A confirmation dialog will be shown the first time an accessibility feature
// is enabled using the specified accelerator key sequence. Only one dialog // is enabled using the specified accelerator key sequence. Only one dialog
@@ -534,6 +536,8 @@ class ASH_EXPORT AccessibilityControllerImpl : public AccessibilityController,
OnDictationKeyboardDialogDismissed(); OnDictationKeyboardDialogDismissed();
} }
void AddShowToastCallbackForTesting(base::RepeatingClosure callback);
private: private:
// Populate |features_| with the feature of the correct type. // Populate |features_| with the feature of the correct type.
void CreateAccessibilityFeatures(); void CreateAccessibilityFeatures();
@@ -647,6 +651,10 @@ class ASH_EXPORT AccessibilityControllerImpl : public AccessibilityController,
// Used to control the Dictation bubble UI. // Used to control the Dictation bubble UI.
std::unique_ptr<DictationBubbleController> dictation_bubble_controller_; std::unique_ptr<DictationBubbleController> dictation_bubble_controller_;
// Used to control accessibility-related notifications.
std::unique_ptr<AccessibilityNotificationController>
accessibility_notification_controller_;
// True if ChromeVox should enable its volume slide gesture. // True if ChromeVox should enable its volume slide gesture.
bool enable_chromevox_volume_slide_gesture_ = false; bool enable_chromevox_volume_slide_gesture_ = false;

@@ -7,6 +7,7 @@
#include "ash/accelerators/accelerator_controller_impl.h" #include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/accessibility/accessibility_controller_impl.h" #include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/shell.h" #include "ash/shell.h"
#include "base/functional/callback.h"
namespace ash { namespace ash {
@@ -52,6 +53,12 @@ void AccessibilityControllerTestApiImpl::DismissDictationKeyboardDialog() {
->DismissDictationKeyboardDialogForTesting(); // IN-TEST ->DismissDictationKeyboardDialogForTesting(); // IN-TEST
} }
void AccessibilityControllerTestApiImpl::AddShowToastCallbackForTesting(
base::RepeatingClosure callback) const {
GetController()->AddShowToastCallbackForTesting(
std::move(callback)); // IN-TEST
}
// static // static
std::unique_ptr<AccessibilityControllerTestApi> std::unique_ptr<AccessibilityControllerTestApi>
AccessibilityControllerTestApi::Create() { AccessibilityControllerTestApi::Create() {

@@ -6,6 +6,7 @@
#define ASH_ACCESSIBILITY_ACCESSIBILITY_CONTROLLER_TEST_API_IMPL_H_ #define ASH_ACCESSIBILITY_ACCESSIBILITY_CONTROLLER_TEST_API_IMPL_H_
#include "ash/public/cpp/test/accessibility_controller_test_api.h" #include "ash/public/cpp/test/accessibility_controller_test_api.h"
#include "base/functional/callback_forward.h"
namespace ash { namespace ash {
@@ -14,12 +15,10 @@ class AccessibilityControllerTestApiImpl
: public AccessibilityControllerTestApi { : public AccessibilityControllerTestApi {
public: public:
AccessibilityControllerTestApiImpl(); AccessibilityControllerTestApiImpl();
AccessibilityControllerTestApiImpl( AccessibilityControllerTestApiImpl(
const AccessibilityControllerTestApiImpl&) = delete; const AccessibilityControllerTestApiImpl&) = delete;
AccessibilityControllerTestApiImpl& operator=( AccessibilityControllerTestApiImpl& operator=(
const AccessibilityControllerTestApiImpl&) = delete; const AccessibilityControllerTestApiImpl&) = delete;
~AccessibilityControllerTestApiImpl() override; ~AccessibilityControllerTestApiImpl() override;
// AccessibilityControllerTestApi: // AccessibilityControllerTestApi:
@@ -29,6 +28,8 @@ class AccessibilityControllerTestApiImpl
bool IsDictationKeboardDialogShowing() const override; bool IsDictationKeboardDialogShowing() const override;
void AcceptDictationKeyboardDialog() override; void AcceptDictationKeyboardDialog() override;
void DismissDictationKeyboardDialog() override; void DismissDictationKeyboardDialog() override;
void AddShowToastCallbackForTesting(
base::RepeatingClosure callback) const override;
}; };
} // namespace ash } // namespace ash

@@ -0,0 +1,56 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/accessibility/accessibility_notification_controller.h"
#include "ash/public/cpp/accessibility_controller_enums.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
const std::string kAccessibilityToastId = "AccessibilityToast";
ToastData GetToastData(AccessibilityToastType type) {
switch (type) {
case AccessibilityToastType::kDictationNoFocusedTextField:
return {/*id=*/kAccessibilityToastId,
/*catalog_name=*/ToastCatalogName::kDictationNoFocusedTextField,
/*text=*/
l10n_util::GetStringUTF16(
IDS_ASH_ACCESSIBILITY_NUDGE_DICTATION_NO_FOCUSED_TEXT_FIELD)};
}
}
} // namespace
AccessibilityNotificationController::AccessibilityNotificationController() =
default;
AccessibilityNotificationController::~AccessibilityNotificationController() =
default;
void AccessibilityNotificationController::ShowToast(
AccessibilityToastType type) {
if (!::features::IsAccessibilityDictationKeyboardImprovementsEnabled()) {
return;
}
Shell::Get()->toast_manager()->Show(GetToastData(type));
if (show_anchored_nudge_callback_for_testing_) {
show_anchored_nudge_callback_for_testing_.Run();
}
}
void AccessibilityNotificationController::AddShowToastCallbackForTesting(
base::RepeatingClosure callback) {
show_anchored_nudge_callback_for_testing_ = std::move(callback);
}
} // namespace ash

@@ -0,0 +1,33 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_ACCESSIBILITY_ACCESSIBILITY_NOTIFICATION_CONTROLLER_H_
#define ASH_ACCESSIBILITY_ACCESSIBILITY_NOTIFICATION_CONTROLLER_H_
#include "ash/ash_export.h"
#include "ash/public/cpp/accessibility_controller_enums.h"
#include "base/functional/callback.h"
namespace ash {
// Class that manages showing notifications for accessibility.
class ASH_EXPORT AccessibilityNotificationController {
public:
AccessibilityNotificationController();
AccessibilityNotificationController(
const AccessibilityNotificationController&) = delete;
AccessibilityNotificationController& operator=(
const AccessibilityNotificationController&) = delete;
~AccessibilityNotificationController();
void ShowToast(AccessibilityToastType type);
void AddShowToastCallbackForTesting(base::RepeatingClosure callback);
private:
base::RepeatingClosure show_anchored_nudge_callback_for_testing_;
};
} // namespace ash
#endif // ASH_ACCESSIBILITY_ACCESSIBILITY_NOTIFICATION_CONTROLLER_H_

@@ -1333,6 +1333,11 @@ Style notes:
With dictation, you can type using your voice. Press the dictation key or select the microphone icon at the bottom of the screen when youre on a text field. Your dictation language is set to <ph name="language">$1<ex>English (United States)</ex></ph>. Speech is sent to Google for processing. You can change the dictation language anytime in Settings > Accessibility. With dictation, you can type using your voice. Press the dictation key or select the microphone icon at the bottom of the screen when youre on a text field. Your dictation language is set to <ph name="language">$1<ex>English (United States)</ex></ph>. Speech is sent to Google for processing. You can change the dictation language anytime in Settings > Accessibility.
</message> </message>
<!-- Accessibility nudges -->
<message name="IDS_ASH_ACCESSIBILITY_NUDGE_DICTATION_NO_FOCUSED_TEXT_FIELD" desc="Displayed in a nudge when Dictation is stopped automatically because there is no focused text field.">
Go to a text field to use Dictation
</message>
<message name="IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD" desc="The label used in the accessibility menu of the system tray to toggle on/off the onscreen keyboard."> <message name="IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD" desc="The label used in the accessibility menu of the system tray to toggle on/off the onscreen keyboard.">
On-screen keyboard On-screen keyboard
</message> </message>

@@ -0,0 +1 @@
cc8d39b5868c8f202c7edc14eedd27e7e64855dd

@@ -273,7 +273,8 @@ enum class ToastCatalogName {
kCopyGifToClipboardAction = 42, kCopyGifToClipboardAction = 42,
// [Deprecated] kVideoConferenceTrayUseWhileDisabled = 43, // [Deprecated] kVideoConferenceTrayUseWhileDisabled = 43,
kBatterySaverDisabled = 44, kBatterySaverDisabled = 44,
kMaxValue = kBatterySaverDisabled kDictationNoFocusedTextField = 45,
kMaxValue = kDictationNoFocusedTextField
}; };
} // namespace ash } // namespace ash

@@ -210,6 +210,9 @@ class ASH_PUBLIC_EXPORT AccessibilityController {
// Cancels all of spoken feedback's current and queued speech immediately. // Cancels all of spoken feedback's current and queued speech immediately.
virtual void SilenceSpokenFeedback() = 0; virtual void SilenceSpokenFeedback() = 0;
// Shows an accessibility-related toast.
virtual void ShowToast(AccessibilityToastType type) = 0;
protected: protected:
AccessibilityController(); AccessibilityController();
virtual ~AccessibilityController(); virtual ~AccessibilityController();

@@ -232,6 +232,12 @@ enum class DictationNotificationType {
kOnlyPumpkinDownloaded, kOnlyPumpkinDownloaded,
}; };
// The types of accessibility-related toasts. This enum should be kept in sync
// with chrome.accessibilityPrivate.ToastType.
enum class AccessibilityToastType {
kDictationNoFocusedTextField,
};
} // namespace ash } // namespace ash
#endif // ASH_PUBLIC_CPP_ACCESSIBILITY_CONTROLLER_ENUMS_H_ #endif // ASH_PUBLIC_CPP_ACCESSIBILITY_CONTROLLER_ENUMS_H_

@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include "ash/ash_export.h" #include "ash/ash_export.h"
#include "base/functional/callback_forward.h"
namespace ash { namespace ash {
@@ -27,6 +28,8 @@ class ASH_EXPORT AccessibilityControllerTestApi {
virtual bool IsDictationKeboardDialogShowing() const = 0; virtual bool IsDictationKeboardDialogShowing() const = 0;
virtual void AcceptDictationKeyboardDialog() = 0; virtual void AcceptDictationKeyboardDialog() = 0;
virtual void DismissDictationKeyboardDialog() = 0; virtual void DismissDictationKeyboardDialog() = 0;
virtual void AddShowToastCallbackForTesting(
base::RepeatingClosure callback) const = 0;
}; };
} // namespace ash } // namespace ash

@@ -67,6 +67,17 @@ namespace {
namespace accessibility_private = ::extensions::api::accessibility_private; namespace accessibility_private = ::extensions::api::accessibility_private;
using ::ash::AccessibilityManager; using ::ash::AccessibilityManager;
ash::AccessibilityToastType ConvertToastType(
accessibility_private::ToastType type) {
switch (type) {
case accessibility_private::ToastType::
TOAST_TYPE_DICTATIONNOFOCUSEDTEXTFIELD:
return ash::AccessibilityToastType::kDictationNoFocusedTextField;
case accessibility_private::ToastType::TOAST_TYPE_NONE:
NOTREACHED_NORETURN();
}
}
ash::DictationBubbleHintType ConvertDictationHintType( ash::DictationBubbleHintType ConvertDictationHintType(
accessibility_private::DictationBubbleHintType hint_type) { accessibility_private::DictationBubbleHintType hint_type) {
switch (hint_type) { switch (hint_type) {
@@ -782,6 +793,15 @@ AccessibilityPrivateSetVirtualKeyboardVisibleFunction::Run() {
return RespondNow(NoArguments()); return RespondNow(NoArguments());
} }
ExtensionFunction::ResponseAction AccessibilityPrivateShowToastFunction::Run() {
absl::optional<accessibility_private::ShowToast::Params> params(
accessibility_private::ShowToast::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
ash::AccessibilityController::Get()->ShowToast(
ConvertToastType(params->type));
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction ExtensionFunction::ResponseAction
AccessibilityPrivateShowConfirmationDialogFunction::Run() { AccessibilityPrivateShowConfirmationDialogFunction::Run() {
absl::optional<accessibility_private::ShowConfirmationDialog::Params> params = absl::optional<accessibility_private::ShowConfirmationDialog::Params> params =

@@ -240,6 +240,14 @@ class AccessibilityPrivateSetVirtualKeyboardVisibleFunction
ACCESSIBILITY_PRIVATE_SETVIRTUALKEYBOARDVISIBLE) ACCESSIBILITY_PRIVATE_SETVIRTUALKEYBOARDVISIBLE)
}; };
// API function that displays an accessibility-related toast.
class AccessibilityPrivateShowToastFunction : public ExtensionFunction {
~AccessibilityPrivateShowToastFunction() override = default;
ResponseAction Run() override;
DECLARE_EXTENSION_FUNCTION("accessibilityPrivate.showToast",
ACCESSIBILITY_PRIVATE_SHOWTOAST)
};
// API function that shows a confirmation dialog, with callbacks for // API function that shows a confirmation dialog, with callbacks for
// confirm/cancel. // confirm/cancel.
class AccessibilityPrivateShowConfirmationDialogFunction class AccessibilityPrivateShowConfirmationDialogFunction

@@ -8,6 +8,7 @@
#include "ash/constants/ash_pref_names.h" #include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/system_tray_test_api.h" #include "ash/public/cpp/system_tray_test_api.h"
#include "ash/public/cpp/test/accessibility_controller_test_api.h"
#include "ash/root_window_controller.h" #include "ash/root_window_controller.h"
#include "ash/shell.h" #include "ash/shell.h"
#include "ash/system/accessibility/dictation_button_tray.h" #include "ash/system/accessibility/dictation_button_tray.h"
@@ -316,6 +317,8 @@ class DictationTestBase : public InProcessBrowserTest,
utils_->set_wait_for_accessibility_common_extension_load_(use); utils_->set_wait_for_accessibility_common_extension_load_(use);
} }
DictationTestUtils* utils() { return utils_.get(); }
private: private:
std::unique_ptr<DictationTestUtils> utils_; std::unique_ptr<DictationTestUtils> utils_;
base::test::ScopedFeatureList scoped_feature_list_; base::test::ScopedFeatureList scoped_feature_list_;
@@ -2240,4 +2243,109 @@ IN_PROC_BROWSER_TEST_P(DictationFormattedContentEditableTest,
SendFinalResultAndWaitForEditableValue("was one", "This was one test"); SendFinalResultAndWaitForEditableValue("was one", "This was one test");
} }
class AccessibilityToastCallbackManager {
public:
void OnToastShown() { run_loop_.Quit(); }
void WaitForToastShown() { run_loop_.Run(); }
private:
base::RunLoop run_loop_;
};
class DictationKeyboardImprovementsTest : public DictationTestBase {
public:
DictationKeyboardImprovementsTest() = default;
~DictationKeyboardImprovementsTest() override = default;
DictationKeyboardImprovementsTest(const DictationKeyboardImprovementsTest&) =
delete;
DictationKeyboardImprovementsTest& operator=(
const DictationKeyboardImprovementsTest&) = delete;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
DictationTestBase::SetUpCommandLine(command_line);
std::vector<base::test::FeatureRef> enabled_features{
::features::kAccessibilityDictationKeyboardImprovements};
scoped_feature_list_.InitWithFeatures(
enabled_features, std::vector<base::test::FeatureRef>());
}
void SetUpOnMainThread() override {
DictationTestBase::SetUpOnMainThread();
test_api_ = AccessibilityControllerTestApi::Create();
callback_manager_ = std::make_unique<AccessibilityToastCallbackManager>();
test_api_->AddShowToastCallbackForTesting(
base::BindRepeating(&AccessibilityToastCallbackManager::OnToastShown,
base::Unretained(callback_manager_.get())));
// Tab away from the editable.
ASSERT_NO_FATAL_FAILURE(ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
/*window=*/nullptr, /*key=*/ui::KeyboardCode::VKEY_TAB,
/*control=*/false,
/*shift=*/false, /*alt=*/false, /*command=*/false)));
// Reduce the no focused IME timeout so that the nudge will be shown
// promptly.
ExecuteAccessibilityCommonScript(
"testSupport.setNoFocusedImeTimeout(500);");
}
void WaitForToastShown() { callback_manager_->WaitForToastShown(); }
private:
std::unique_ptr<AccessibilityControllerTestApi> test_api_;
std::unique_ptr<AccessibilityToastCallbackManager> callback_manager_;
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
NetworkDictationKeyboardImprovementsTest,
DictationKeyboardImprovementsTest,
::testing::Values(TestConfig(speech::SpeechRecognitionType::kNetwork,
EditableType::kInput)));
// Verifies that a nudge is shown in the system UI when Dictation is toggled
// when there is no focused editable.
IN_PROC_BROWSER_TEST_P(DictationKeyboardImprovementsTest,
ToggledWithNoFocusShowsNudge) {
// Disable the console observer because toggling Dictation in the following
// manner will cause an error to be emitted to the console.
utils()->DisableConsoleObserver();
ToggleDictationWithKeystroke();
WaitForToastShown();
WaitForRecognitionStopped();
}
// Verifies that ChromeVox announces a message when when Dictation is toggled
// when there is no focused editable.
// TODO(b:259352600): Re-enable this test on MSAN.
#if defined(MEMORY_SANITIZER)
#define MAYBE_ToggledWithNoFocusTriggersSpeech \
DISABLED_ToggledWithNoFocusTriggersSpeech
#else
#define MAYBE_ToggledWithNoFocusTriggersSpeech ToggledWithNoFocusTriggersSpeech
#endif
IN_PROC_BROWSER_TEST_P(DictationKeyboardImprovementsTest,
MAYBE_ToggledWithNoFocusTriggersSpeech) {
// Setup 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());
// Disable the console observer because toggling Dictation in the following
// manner will cause an error to be emitted to the console.
utils()->DisableConsoleObserver();
ToggleDictationWithKeystroke();
WaitForToastShown();
WaitForRecognitionStopped();
// Assert speech from ChromeVox.
sm.ExpectSpeechPattern("*Go to a text field to use Dictation*");
sm.Replay();
}
} // namespace ash } // namespace ash

@@ -179,7 +179,7 @@ void DictationTestUtils::EnableDictation(Browser* browser) {
// Increase Dictation's NO_FOCUSED_IME timeout to reduce flakiness on slower // Increase Dictation's NO_FOCUSED_IME timeout to reduce flakiness on slower
// builds. // builds.
std::string script = "testSupport.increaseNoFocusedImeTimeout();"; std::string script = "testSupport.setNoFocusedImeTimeout(20 * 1000);";
ExecuteAccessibilityCommonScript(script); ExecuteAccessibilityCommonScript(script);
// Dictation will request a Pumpkin install when it starts up. Wait for // Dictation will request a Pumpkin install when it starts up. Wait for

@@ -94,6 +94,10 @@ class DictationTestUtils {
int GetCommitTextCallCount(); int GetCommitTextCallCount();
void WaitForCommitText(const std::u16string& value); void WaitForCommitText(const std::u16string& value);
// TODO(b:259352600): Instead of disabling the observer, change this to
// allow specific messages.
void DisableConsoleObserver() { console_observer_.reset(); }
// Sets whether or not we should wait for the accessibility common extension // Sets whether or not we should wait for the accessibility common extension
// to load when enabling Dictation. This should be true in almost all cases. // to load when enabling Dictation. This should be true in almost all cases.
// However, there are times when we don't want to wait for accessibility // However, there are times when we don't want to wait for accessibility

@@ -202,7 +202,7 @@ export class Dictation {
} }
this.setStopTimeout_( this.setStopTimeout_(
Dictation.Timeouts.NO_FOCUSED_IME_MS, Dictation.Timeouts.NO_FOCUSED_IME_MS,
'Dictation stopped automatically: No focused IME'); Dictation.StopReason.NO_FOCUSED_IME);
this.inputController_.connect(() => this.maybeStartSpeechRecognition_()); this.inputController_.connect(() => this.maybeStartSpeechRecognition_());
} }
@@ -262,18 +262,21 @@ export class Dictation {
/** /**
* Sets the timeout to stop Dictation. * Sets the timeout to stop Dictation.
* @param {number} durationMs * @param {number} durationMs
* @param {string=} debugInfo Optional debugging information for why Dictation * @param {Dictation.StopReason=} reason Optional reason for why Dictation
* stopped automatically. * stopped automatically.
* @private * @private
*/ */
setStopTimeout_(durationMs, debugInfo) { setStopTimeout_(durationMs, reason) {
if (this.stopTimeoutId_ !== null) { if (this.stopTimeoutId_ !== null) {
clearTimeout(this.stopTimeoutId_); clearTimeout(this.stopTimeoutId_);
} }
this.stopTimeoutId_ = setTimeout(() => { this.stopTimeoutId_ = setTimeout(() => {
this.stopDictation_(/*notify=*/ true); this.stopDictation_(/*notify=*/ true);
if (debugInfo) {
console.log(debugInfo); if (reason === Dictation.StopReason.NO_FOCUSED_IME) {
chrome.accessibilityPrivate.showToast(
chrome.accessibilityPrivate.ToastType
.DICTATION_NO_FOCUSED_TEXT_FIELD);
} }
}, durationMs); }, durationMs);
} }
@@ -540,12 +543,9 @@ export class Dictation {
InputController.IME_ENGINE_ID); InputController.IME_ENGINE_ID);
} }
/** /** Used to set the NO_FOCUSED_IME_MS timeout for testing purposes only. */
* Used to increase the NO_FOCUSED_IME_MS timeout to reduce the flakiness of setNoFocusedImeTimeoutForTesting(duration) {
* Dictation tests on slower builds. For testing purposes only. Dictation.Timeouts.NO_FOCUSED_IME_MS = duration;
*/
increaseNoFocusedImeTimeoutForTesting() {
Dictation.Timeouts.NO_FOCUSED_IME_MS = 20 * 1000;
} }
/** /**
@@ -628,3 +628,8 @@ Dictation.Timeouts = {
NO_NEW_SPEECH_MS: 5 * 1000, NO_NEW_SPEECH_MS: 5 * 1000,
NO_FOCUSED_IME_MS: 1000, NO_FOCUSED_IME_MS: 1000,
}; };
/** @enum {string} */
Dictation.StopReason = {
NO_FOCUSED_IME: 'Dictation stopped automatically: No focused IME',
};

@@ -116,7 +116,7 @@ DictationE2ETestBase = class extends E2ETestBase {
accessibilityCommon.dictation_.disablePumpkinForTesting(); accessibilityCommon.dictation_.disablePumpkinForTesting();
// Increase Dictation's NO_FOCUSED_IME timeout to reduce flakiness on slower // Increase Dictation's NO_FOCUSED_IME timeout to reduce flakiness on slower
// builds. // builds.
accessibilityCommon.dictation_.increaseNoFocusedImeTimeoutForTesting(); accessibilityCommon.dictation_.setNoFocusedImeTimeoutForTesting(20 * 1000);
} }
/** @override */ /** @override */

@@ -20,9 +20,9 @@ class DictationTestSupport {
domAutomationController.send('ready'); domAutomationController.send('ready');
} }
/** Increases Dictation timeouts for test stability. */ /** Sets Dictation timeouts for test stability. */
increaseNoFocusedImeTimeout() { setNoFocusedImeTimeout(duration) {
this.dictation_.increaseNoFocusedImeTimeoutForTesting(); this.dictation_.setNoFocusedImeTimeoutForTesting(duration);
this.notifyCcTests_(); this.notifyCcTests_();
} }

@@ -73,6 +73,10 @@ class MockAccessibilityPrivate {
this.SyntheticKeyboardEventType = {KEYDOWN: 'keydown', KEYUP: 'keyup'}; this.SyntheticKeyboardEventType = {KEYDOWN: 'keydown', KEYUP: 'keyup'};
this.ToastType = {
DICTATION_NO_FOCUSED_TEXT_FIELD: 'dictationNoFocusedTextField',
};
/** @private {function<number, number>} */ /** @private {function<number, number>} */
this.boundsListener_ = null; this.boundsListener_ = null;
@@ -484,4 +488,7 @@ class MockAccessibilityPrivate {
await getFileBytes(`${pumpkinDir}/es_es/pumpkin_config.binarypb`); await getFileBytes(`${pumpkinDir}/es_es/pumpkin_config.binarypb`);
MockAccessibilityPrivate.pumpkinData_ = data; MockAccessibilityPrivate.pumpkinData_ = data;
} }
/** @param {!chrome.accessibilityPrivate.ToastType} type */
showToast(type) {}
} }

@@ -117,3 +117,5 @@ void FakeAccessibilityController::UpdateDictationBubble(
const absl::optional<std::vector<ash::DictationBubbleHintType>>& hints) {} const absl::optional<std::vector<ash::DictationBubbleHintType>>& hints) {}
void FakeAccessibilityController::SilenceSpokenFeedback() {} void FakeAccessibilityController::SilenceSpokenFeedback() {}
void FakeAccessibilityController::ShowToast(ash::AccessibilityToastType type) {}

@@ -77,6 +77,7 @@ class FakeAccessibilityController : ash::AccessibilityController {
const absl::optional<std::vector<ash::DictationBubbleHintType>>& hints) const absl::optional<std::vector<ash::DictationBubbleHintType>>& hints)
override; override;
void SilenceSpokenFeedback() override; void SilenceSpokenFeedback() override;
void ShowToast(ash::AccessibilityToastType type) override;
private: private:
bool was_client_set_ = false; bool was_client_set_ = false;

@@ -292,6 +292,13 @@
} }
} }
}, },
{
"id": "ToastType",
"type": "string",
"enum": [
"dictationNoFocusedTextField"
]
},
{ {
"id": "DlcType", "id": "DlcType",
"type": "string", "type": "string",
@@ -894,6 +901,16 @@
} }
] ]
} }
},
{
"name": "showToast",
"type": "function",
"description": "Displays an accessibility-related toast.",
"parameters": [{
"name": "type",
"$ref": "ToastType",
"description": "The type of toast to show."
}]
} }
], ],
"events": [ "events": [

@@ -1880,6 +1880,7 @@ enum HistogramValue {
OS_DIAGNOSTICS_RUNBLUETOOTHSCANNINGROUTINE = 1818, OS_DIAGNOSTICS_RUNBLUETOOTHSCANNINGROUTINE = 1818,
OS_DIAGNOSTICS_RUNBLUETOOTHPAIRINGROUTINE = 1819, OS_DIAGNOSTICS_RUNBLUETOOTHPAIRINGROUTINE = 1819,
FILEMANAGERPRIVATE_DISMISSIOTASK = 1820, FILEMANAGERPRIVATE_DISMISSIOTASK = 1820,
ACCESSIBILITY_PRIVATE_SHOWTOAST = 1821,
// Last entry: Add new entries above, then run: // Last entry: Add new entries above, then run:
// tools/metrics/histograms/update_extension_histograms.py // tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY ENUM_BOUNDARY

@@ -339,6 +339,13 @@ chrome.accessibilityPrivate.DictationBubbleHintType = {
*/ */
chrome.accessibilityPrivate.DictationBubbleProperties; chrome.accessibilityPrivate.DictationBubbleProperties;
/**
* @enum {string}
*/
chrome.accessibilityPrivate.ToastType = {
DICTATION_NO_FOCUSED_TEXT_FIELD: 'dictationNoFocusedTextField',
};
/** /**
* @enum {string} * @enum {string}
*/ */
@@ -657,6 +664,13 @@ chrome.accessibilityPrivate.getDlcContents = function(dlc, callback) {};
*/ */
chrome.accessibilityPrivate.isLacrosPrimary = function(callback) {}; chrome.accessibilityPrivate.isLacrosPrimary = function(callback) {};
/**
* Displays an accessibility-related toast.
* @param {!chrome.accessibilityPrivate.ToastType} type The type of toast to
* show.
*/
chrome.accessibilityPrivate.showToast = function(type) {};
/** /**
* Fired whenever ChromeVox should output introduction. * Fired whenever ChromeVox should output introduction.
* @type {!ChromeEvent} * @type {!ChromeEvent}

@@ -37589,6 +37589,7 @@ Called by update_extension_histograms.py.-->
<int value="1818" label="OS_DIAGNOSTICS_RUNBLUETOOTHSCANNINGROUTINE"/> <int value="1818" label="OS_DIAGNOSTICS_RUNBLUETOOTHSCANNINGROUTINE"/>
<int value="1819" label="OS_DIAGNOSTICS_RUNBLUETOOTHPAIRINGROUTINE"/> <int value="1819" label="OS_DIAGNOSTICS_RUNBLUETOOTHPAIRINGROUTINE"/>
<int value="1820" label="FILEMANAGERPRIVATE_DISMISSIOTASK"/> <int value="1820" label="FILEMANAGERPRIVATE_DISMISSIOTASK"/>
<int value="1821" label="ACCESSIBILITY_PRIVATE_SHOWTOAST"/>
</enum> </enum>
<enum name="ExtensionIconState"> <enum name="ExtensionIconState">
@@ -77776,6 +77777,8 @@ Called by update_net_trust_anchors.py.-->
<int value="18" label="Scalable IPH Bubble"/> <int value="18" label="Scalable IPH Bubble"/>
<int value="19" <int value="19"
label="Video Conference Tray Camera And Microphone Use While Disabled"/> label="Video Conference Tray Camera And Microphone Use While Disabled"/>
<int value="20" label="Multitask Menu Clamshell"/>
<int value="21" label="Multitask Menu Tablet"/>
</enum> </enum>
<enum name="NukeProfileResult"> <enum name="NukeProfileResult">
@@ -104470,6 +104473,7 @@ would be helpful to identify which type is being sent.
<int value="41" label="Video Conference Tray Speak-On-Mute Detected"/> <int value="41" label="Video Conference Tray Speak-On-Mute Detected"/>
<int value="42" label="Copy Gif To Clipboard Action"/> <int value="42" label="Copy Gif To Clipboard Action"/>
<int value="44" label="Battery Saver Disabled"/> <int value="44" label="Battery Saver Disabled"/>
<int value="45" label="Dictation No Focused Text Field"/>
</enum> </enum>
<enum name="TokenBinding.KeyMatch"> <enum name="TokenBinding.KeyMatch">