0

welcome_tour: Add A11y support

Enable ChromeVox will not prevent tour to show.

This cl adds a different set of string for accessible labels in the help
bubble view.

Bug: b:323363476
Test: Added unittest and Tested on the device
Change-Id: I28e7e62622a69b9912d18f5abfe09ee23a9213d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5315597
Reviewed-by: David Black <dmblack@google.com>
Commit-Queue: Tao Wu <wutao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1269979}
This commit is contained in:
Tao Wu
2024-03-08 02:39:41 +00:00
committed by Chromium LUCI CQ
parent eafaf07ab1
commit 7519458ece
34 changed files with 570 additions and 130 deletions

@ -3114,6 +3114,7 @@ component("ash") {
"//chromeos/components/sensors:sensors",
"//chromeos/components/sensors/mojom",
"//chromeos/components/webauthn",
"//chromeos/constants",
"//chromeos/crosapi/cpp",
"//chromeos/dbus/constants",
"//chromeos/dbus/init",

@ -7443,25 +7443,46 @@ To shut down the device, press and hold the power button on the device again.
No thanks
</message>
<message name="IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT" desc="Text shown in the body of the dialog as part of the System UI Welcome Tour.">
Take a quick tour to learn how to get around your Chromebook. Get up and running in 6 steps.
Take a quick tour to learn how to get around your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph>. Get up and running in 5 steps.
</message>
<message name="IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT" desc="Text shown as the Welcome Tour dialog title.">
Hi there. Chromebook is a little different.
Hi there. <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph> is a little different.
</message>
<message name="IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the Explore app as part of the System UI Welcome Tour.">
Those were the basics! Continue in Explore, our built-in app for tips and help. Youll find tips for getting started, recommended apps, special offers, and the newest Chromebook features.
Those were the basics! Continue in Explore, our built-in app for tips and help. Youll find tips for getting started, recommended apps, special offers, and the newest <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph> features.
</message>
<message name="IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the home button as part of the System UI Welcome Tour.">
Your Chromebook comes with built-in apps to help you get stuff done. Find your apps in the Launcher.
<message name="IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_ACCNAME" desc="The accessible name of a help bubble anchored to the home button as part of the System UI Welcome Tour.">
Tour step 3 of 5. Use apps to do everything you need on your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph>. You can find your apps in the Launcher. Press Alt + Shift + L to focus on the Launcher button.
</message>
<message name="IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_CHROMEBOOK" desc="Text shown in the body of a help bubble anchored to the home button as part of the System UI Welcome Tour.">
Use apps to do everything you need on your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph>. You can find your apps in the Launcher. You can also press the Launcher key (above the left Shift key) on the keyboard.
</message>
<message name="IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_OTHER_DEVICE_TYPES" desc="Text shown in the body of a help bubble anchored to the home button as part of the System UI Welcome Tour.">
Use apps to do everything you need on your <ph name="PRODUCT_NAME">$1<ex>Chromebox</ex></ph>. You can find your apps in the Launcher.
</message>
<message name="IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT" desc="Text which is overridden and so not shown in the body of a help bubble as part of the System UI Welcome Tour but which is still required to be provided to satisfy API requirements." translateable="false">
''' '''
</message>
<message name="IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_ACCNAME" desc="The accessible name of a help bubble anchored to the search box as part of the System UI Welcome Tour.">
Tour step 4 of 5. Once Launcher is activated, youll get an enhanced search bar. You can start typing to search for your files, apps, and more. You can also get answers to questions about your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph>.
</message>
<message name="IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the search box as part of the System UI Welcome Tour.">
Search for your files, apps, and more in the Launcher. You can also get answers to questions about your Chromebook.
Search for your files, apps, and more in the Launcher. You can also get answers to questions about your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph>.
</message>
<message name="IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_ACCNAME" desc="The accessible name of a help bubble anchored to the Settings app as part of the System UI Welcome Tour">
Tour step 5 of 5. You can find your device Settings in Launcher. Try customizing your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph> in Settings like changing your wallpaper or setting a screen saver.
</message>
<message name="IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the Settings app as part of the System UI Welcome Tour">
Customize and personalize your Chromebook in Settings. Try changing your wallpaper or setting a screen saver.
Customize and personalize your <ph name="PRODUCT_NAME">$1<ex>Chromebook</ex></ph> in Settings. Try changing your wallpaper or setting a screen saver.
</message>
<message name="IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_ACCNAME" desc="The accessible name of a help bubble anchored to the shelf as part of the System UI Welcome Tour.">
Tour step 1 of 5. Your pinned and open apps are on the shelf located at the bottom of your screen. Press Alt + Shift + L then tab to focus on shelf items.
</message>
<message name="IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the shelf as part of the System UI Welcome Tour.">
Your pinned and open apps are on the Shelf. To pin an app to the Shelf, right-click an app or tap your touchpad with two fingers.
Your pinned and open apps are on the shelf. To pin an app to the shelf, right-click an app or tap your touchpad with two fingers.
</message>
<message name="IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_ACCNAME" desc="The accessible name of a help bubble anchored to the status area as part of the System UI Welcome Tour.">
Tour step 2 of 5. Frequently used controls like Wi-Fi, Bluetooth and volume are in Quick Settings. You can also go here to take screenshots. Press Alt + Shift + S to open Quick Settings.
</message>
<message name="IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT" desc="Text shown in the body of a help bubble anchored to the status area as part of the System UI Welcome Tour.">
Frequently used controls like Wi-Fi, Bluetooth and volume are in Quick Settings. You can also go here to take screenshots.

@ -1 +1 @@
91f005bedaacf5f2aad71e6d14d785516e314450
b08c55b3ed3576880e8392b6a93f0d87be670d1d

@ -1 +1 @@
91f005bedaacf5f2aad71e6d14d785516e314450
abdd1b8775319d9448142fdc111a167cf7d188e1

@ -1 +1 @@
4d26e1f3f321fe1847588fbde7485635fad23855
3e66510f6e69fdb7d266853f790053f92bb45bf3

@ -0,0 +1 @@
50db39a2b7ba065eccee1cf516f22df2eb6b3fd2

@ -1 +0,0 @@
d3f894789567c27afce7f35ce6df99cdcdd771fc

@ -0,0 +1 @@
a766a35a25a15ca955f256bba66c10f7e8e6a2ff

@ -0,0 +1 @@
50f131a2b0a93fc71e26b1300967be48744f3e77

@ -0,0 +1 @@
e8894f733d95248c62b26dab3d4105e930c811ea

@ -1 +1 @@
af458f270c6b49e0451e8cd61843150f2ae57f21
72af7447d4ad45234d7c88f495871cb5b3477b3e

@ -0,0 +1 @@
5802fb73976b6c11ecc58d80caa101e8bf0fe1ca

@ -1 +1 @@
671909ea0a3c0362619d421860c3d964f5a454e1
c17dd9de3294d5e389772f8ed1146bcc85a4bb88

@ -0,0 +1 @@
26f96d0e6ac397230594df7cb2b33be2bd3c0ef1

@ -1 +1 @@
67d12523136eb3cdd936b127e4fcc9791ecbd4cd
305612f7a60680c1683681b3a5a7ef912c8f86a6

@ -0,0 +1 @@
6ec4e1ca2a23bb080ed7480fc3c1f0f284cd4530

@ -1 +1 @@
1f223010c05927e508e24924da37f601a9065089
6e4ef87469c088cbbe1ff97a0f2593b0ff3d8ff0

@ -2882,6 +2882,12 @@ BASE_FEATURE(kWelcomeTour, "WelcomeTour", base::FEATURE_DISABLED_BY_DEFAULT);
const base::FeatureParam<bool> kWelcomeTourEnabledCounterfactually{
&kWelcomeTour, "is-counterfactual", false};
// Whether ChromeVox is supported in the Welcome Tour that walks new users
// through ChromeOS System UI.
BASE_FEATURE(kWelcomeTourChromeVoxSupported,
"WelcomeTourChromeVoxSupported",
base::FEATURE_DISABLED_BY_DEFAULT);
// Forces user eligibility for the Welcome Tour that walks new users through
// ChromeOS System UI. Enabling this flag has no effect unless `kWelcomeTour` is
// also enabled.
@ -4417,6 +4423,11 @@ bool IsWallpaperPerDeskEnabled() {
return base::FeatureList::IsEnabled(kWallpaperPerDesk);
}
bool IsWelcomeTourChromeVoxSupported() {
return IsWelcomeTourEnabled() &&
base::FeatureList::IsEnabled(kWelcomeTourChromeVoxSupported);
}
bool IsWelcomeTourEnabled() {
return base::FeatureList::IsEnabled(kWelcomeTour);
}

@ -874,6 +874,8 @@ COMPONENT_EXPORT(ASH_CONSTANTS)
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWelcomeTour);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWelcomeTourChromeVoxSupported);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWelcomeTourForceUserEligibility);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWifiConnectMacAddressRandomization);
@ -1284,6 +1286,7 @@ COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWallpaperFastRefreshEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS)
bool IsWallpaperGooglePhotosSharedAlbumsEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWallpaperPerDeskEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWelcomeTourChromeVoxSupported();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWelcomeTourEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWelcomeTourEnabledCounterfactually();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWelcomeTourForceUserEligibilityEnabled();

@ -31,7 +31,9 @@ namespace ash::user_education_util {
namespace {
// Keys used in `user_education::HelpBubbleParams::ExtendedProperties`.
constexpr char kHelpBubbleAccessibleNameKey[] = "helpBubbleAccessibleName";
constexpr char kHelpBubbleBodyIconKey[] = "helpBubbleBodyIcon";
constexpr char kHelpBubbleBodyTextKey[] = "helpBubbleBodyText";
constexpr char kHelpBubbleIdKey[] = "helpBubbleId";
constexpr char kHelpBubbleModalTypeKey[] = "helpBubbleModalType";
constexpr char kHelpBubbleStyleKey[] = "helpBubbleStyle";
@ -117,10 +119,36 @@ user_education::HelpBubbleParams::ExtendedProperties CreateExtendedProperties(
return extended_properties;
}
user_education::HelpBubbleParams::ExtendedProperties
CreateExtendedPropertiesWithAccessibleName(const std::string& accessible_name) {
user_education::HelpBubbleParams::ExtendedProperties extended_properties;
extended_properties.values().Set(kHelpBubbleAccessibleNameKey,
accessible_name);
return extended_properties;
}
user_education::HelpBubbleParams::ExtendedProperties
CreateExtendedPropertiesWithBodyText(const std::string& body_text) {
user_education::HelpBubbleParams::ExtendedProperties extended_properties;
extended_properties.values().Set(kHelpBubbleBodyTextKey, body_text);
return extended_properties;
}
const AccountId& GetAccountId(const UserSession* user_session) {
return user_session ? user_session->user_info.account_id : EmptyAccountId();
}
std::optional<std::string> GetHelpBubbleAccessibleName(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties) {
if (const std::string* help_bubble_accessible_name =
extended_properties.values().FindString(
kHelpBubbleAccessibleNameKey)) {
return *help_bubble_accessible_name;
}
return std::nullopt;
}
std::optional<std::reference_wrapper<const gfx::VectorIcon>>
GetHelpBubbleBodyIcon(
const user_education::HelpBubbleParams::ExtendedProperties&
@ -135,6 +163,16 @@ GetHelpBubbleBodyIcon(
return std::nullopt;
}
std::optional<std::string> GetHelpBubbleBodyText(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties) {
if (const std::string* help_bubble_body_text =
extended_properties.values().FindString(kHelpBubbleBodyTextKey)) {
return *help_bubble_body_text;
}
return std::nullopt;
}
HelpBubbleId GetHelpBubbleId(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties) {

@ -57,6 +57,14 @@ CreateExtendedProperties(HelpBubbleStyle help_bubble_style);
ASH_EXPORT user_education::HelpBubbleParams::ExtendedProperties
CreateExtendedProperties(ui::ModalType modal_type);
// Returns extended properties for a help bubble having set `accessible_name`.
ASH_EXPORT user_education::HelpBubbleParams::ExtendedProperties
CreateExtendedPropertiesWithAccessibleName(const std::string& accessible_name);
// Returns extended properties for a help bubble having set `body_text`.
ASH_EXPORT user_education::HelpBubbleParams::ExtendedProperties
CreateExtendedPropertiesWithBodyText(const std::string& body_text);
/*
Creates an extended properties instance by merging `properties`.
@ -79,6 +87,13 @@ CreateExtendedProperties(Properties&&... properties) {
// `user_session` is `nullptr`, `EmptyAccountId()` is returned.
ASH_EXPORT const AccountId& GetAccountId(const UserSession* user_session);
// Returns help bubble accessible name from the specified `extended_properties`.
// If the specified `extended_properties` does not contain help bubble
// accessible name, an absent value is returned.
ASH_EXPORT std::optional<std::string> GetHelpBubbleAccessibleName(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties);
// Returns help bubble body icon from the specified `external_properties`. If
// the specified `external_properties` does not contain a help bubble body icon,
// an absent value is returned.
@ -87,6 +102,13 @@ GetHelpBubbleBodyIcon(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties);
// Returns help bubble body text from the specified `extended_properties`.
// If the specified `extended_properties` does not contain help bubble
// body text, an absent value is returned.
ASH_EXPORT std::optional<std::string> GetHelpBubbleBodyText(
const user_education::HelpBubbleParams::ExtendedProperties&
extended_properties);
// Returns help bubble ID from the specified `extended_properties`.
ASH_EXPORT HelpBubbleId GetHelpBubbleId(
const user_education::HelpBubbleParams::ExtendedProperties&

@ -121,6 +121,36 @@ TEST_F(UserEducationUtilTest, ExtendedPropertiesWithStyle) {
std::nullopt);
}
// Verifies that `CreateExtendedPropertiesWithAccessibleName()` can be used to
// create extended properties for a help bubble having set accessible name, and
// that `GetHelpBubbleAccessibleName()` can be used to retrieve help bubble
// accessible name from extended properties.
TEST_F(UserEducationUtilTest, ExtendedPropertiesWithAccessibleName) {
std::string accessible_name = "Accessible Name";
EXPECT_EQ(GetHelpBubbleAccessibleName(
CreateExtendedPropertiesWithAccessibleName(accessible_name)),
accessible_name);
// It is permissible to query help bubble accessible name even when absent.
EXPECT_EQ(GetHelpBubbleAccessibleName(HelpBubbleParams::ExtendedProperties()),
std::nullopt);
}
// Verifies that `CreateExtendedPropertiesWithBodyText()` can be used to create
// extended properties for a help bubble having set body text, and that
// `GetHelpBubbleBodyText()` can be used to retrieve help bubble body text from
// extended properties.
TEST_F(UserEducationUtilTest, ExtendedPropertiesWithBodyText) {
std::string body_text = "Body Text";
EXPECT_EQ(
GetHelpBubbleBodyText(CreateExtendedPropertiesWithBodyText(body_text)),
body_text);
// It is permissible to query help bubble body text even when absent.
EXPECT_EQ(GetHelpBubbleBodyText(HelpBubbleParams::ExtendedProperties()),
std::nullopt);
}
// Verifies that `ToString()` is working as intended.
TEST_F(UserEducationUtilTest, ToString) {
std::set<std::string> tutorial_id_strs;

@ -7,6 +7,7 @@
#include <initializer_list>
#include <memory>
#include <numeric>
#include <optional>
#include <string>
#include <utility>
@ -22,6 +23,7 @@
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "components/strings/grit/components_strings.h"
@ -300,13 +302,37 @@ HelpBubbleViewAsh::HelpBubbleViewAsh(
}
SetCancelCallback(std::move(params.dismiss_callback));
accessible_name_ = params.title_text;
if (!accessible_name_.empty()) {
accessible_name_ += u". ";
// A body text provided from extended properties should take precedence
// over the default body text provided from help bubble `params` since
// extended properties are the ChromeOS-specific mechanism for overriding
// platform agnostic behaviors.
std::u16string body_text;
if (auto body_text_from_extended_properties =
user_education_util::GetHelpBubbleBodyText(
params.extended_properties)) {
body_text = base::UTF8ToUTF16(body_text_from_extended_properties.value());
} else {
body_text = params.body_text;
}
accessible_name_ += params.screenreader_text.empty()
? params.body_text
: params.screenreader_text;
// An accessible name provided from extended properties should take precedence
// over the default accessible name provided from help bubble `params` since
// extended properties are the ChromeOS-specific mechanism for overriding
// platform agnostic behaviors.
if (auto accessible_name_from_extended_properties =
user_education_util::GetHelpBubbleAccessibleName(
params.extended_properties)) {
accessible_name_ =
base::UTF8ToUTF16(accessible_name_from_extended_properties.value());
} else {
accessible_name_ = params.title_text;
if (!accessible_name_.empty()) {
accessible_name_ += u". ";
}
accessible_name_ +=
params.screenreader_text.empty() ? body_text : params.screenreader_text;
}
screenreader_hint_text_ = params.keyboard_navigation_hint;
// Since we don't have any controls for the user to interact with (we're just
@ -380,16 +406,14 @@ HelpBubbleViewAsh::HelpBubbleViewAsh(
labels_.push_back(
top_text_container->AddChildView(bubble_utils::CreateLabel(
TypographyToken::kCrosBody1, params.title_text)));
views::Label* label =
AddChildViewAt(bubble_utils::CreateLabel(TypographyToken::kCrosBody1,
params.body_text),
GetIndexOf(button_container).value());
views::Label* label = AddChildViewAt(
bubble_utils::CreateLabel(TypographyToken::kCrosBody1, body_text),
GetIndexOf(button_container).value());
labels_.push_back(label);
label->SetProperty(views::kElementIdentifierKey, kBodyTextIdForTesting);
} else {
views::Label* label =
top_text_container->AddChildView(bubble_utils::CreateLabel(
TypographyToken::kCrosBody1, params.body_text));
views::Label* label = top_text_container->AddChildView(
bubble_utils::CreateLabel(TypographyToken::kCrosBody1, body_text));
labels_.push_back(label);
label->SetProperty(views::kElementIdentifierKey, kBodyTextIdForTesting);
}

@ -5,6 +5,7 @@
#include "ash/user_education/welcome_tour/welcome_tour_accelerator_handler.h"
#include "ash/accelerators/ash_accelerator_configuration.h"
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
@ -43,6 +44,11 @@ void WelcomeTourAcceleratorHandler::OnKeyEvent(ui::KeyEvent* event) {
// Block `event` if `action` is not allowed.
event->StopPropagation();
} else if (action_it->aborts_tour) {
if (action_it->action == AcceleratorAction::kToggleSpokenFeedback &&
features::IsWelcomeTourChromeVoxSupported()) {
return;
}
// Aborting the Welcome Tour could affect the enabling of `action`.
// Therefore, abort the Welcome Tour asynchronously.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(

@ -34,7 +34,9 @@
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/elapsed_timer.h"
#include "chromeos/constants/devicetype.h"
#include "components/user_education/common/events.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/tutorial_description.h"
@ -42,7 +44,9 @@
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
@ -68,6 +72,24 @@ CreateHelpBubbleExtendedProperties(HelpBubbleId help_bubble_id) {
/*body_icon=*/gfx::kNoneIcon));
}
user_education::HelpBubbleParams::ExtendedProperties
CreateHelpBubbleExtendedProperties(HelpBubbleId help_bubble_id,
const std::string& body_text) {
return user_education_util::CreateExtendedProperties(
CreateHelpBubbleExtendedProperties(help_bubble_id),
user_education_util::CreateExtendedPropertiesWithBodyText(body_text));
}
user_education::HelpBubbleParams::ExtendedProperties
CreateHelpBubbleExtendedProperties(HelpBubbleId help_bubble_id,
const std::string& accessible_name,
const std::string& body_text) {
return user_education_util::CreateExtendedProperties(
CreateHelpBubbleExtendedProperties(help_bubble_id, body_text),
user_education_util::CreateExtendedPropertiesWithAccessibleName(
accessible_name));
}
base::RepeatingCallback<void(ui::TrackedElement*)> DefaultNextButtonCallback() {
return base::BindRepeating([](ui::TrackedElement* current_anchor) {
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
@ -154,6 +176,8 @@ ui::ElementContext WelcomeTourController::GetInitialElementContext() const {
user_education::TutorialDescription
WelcomeTourController::GetTutorialDescription() const {
const std::u16string product_name = ui::GetChromeOSDeviceName();
user_education::TutorialDescription tutorial_description;
tutorial_description.complete_button_text_id =
IDS_ASH_WELCOME_TOUR_COMPLETE_BUTTON_TEXT;
@ -177,6 +201,7 @@ WelcomeTourController::GetTutorialDescription() const {
user_education::TutorialDescription::BubbleStep(kShelfViewElementId)
.SetBubbleArrow(user_education::HelpBubbleArrow::kBottomCenter)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_BODY_TEXT)
.SetBubbleScreenreaderText(IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_ACCNAME)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourShelf))
.AddCustomNextButton(DefaultNextButtonCallback().Then(
@ -201,6 +226,8 @@ WelcomeTourController::GetTutorialDescription() const {
kUnifiedSystemTrayElementName)
.SetBubbleArrow(user_education::HelpBubbleArrow::kBottomRight)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT)
.SetBubbleScreenreaderText(
IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_ACCNAME)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourStatusArea))
.AddCustomNextButton(DefaultNextButtonCallback().Then(
@ -224,9 +251,20 @@ WelcomeTourController::GetTutorialDescription() const {
tutorial_description.steps.emplace_back(
user_education::TutorialDescription::BubbleStep(kHomeButtonElementName)
.SetBubbleArrow(user_education::HelpBubbleArrow::kBottomLeft)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourHomeButton))
HelpBubbleId::kWelcomeTourHomeButton,
/*accessible_name=*/
l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_ACCNAME,
product_name),
/*body_text=*/
l10n_util::GetStringFUTF8(
(chromeos::GetDeviceType() ==
chromeos::DeviceType::kChromebook)
? IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_CHROMEBOOK
: IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_OTHER_DEVICE_TYPES,
product_name)))
.AddCustomNextButton(base::BindRepeating([](ui::TrackedElement*) {
Shell::Get()->app_list_controller()->Show(
GetPrimaryDisplayId(),
@ -244,9 +282,16 @@ WelcomeTourController::GetTutorialDescription() const {
tutorial_description.steps.emplace_back(
user_education::TutorialDescription::BubbleStep(kSearchBoxViewElementId)
.SetBubbleArrow(user_education::HelpBubbleArrow::kTopCenter)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourSearchBox))
HelpBubbleId::kWelcomeTourSearchBox,
/*accessible_name=*/
l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_ACCNAME, product_name),
/*body_text=*/
l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT,
product_name)))
.AddCustomNextButton(DefaultNextButtonCallback().Then(
base::BindRepeating(&WelcomeTourController::SetCurrentStep,
weak_ptr_factory_.GetMutableWeakPtr(),
@ -264,9 +309,17 @@ WelcomeTourController::GetTutorialDescription() const {
tutorial_description.steps.emplace_back(
user_education::TutorialDescription::BubbleStep(kSettingsAppElementId)
.SetBubbleArrow(user_education::HelpBubbleArrow::kBottomLeft)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourSettingsApp))
HelpBubbleId::kWelcomeTourSettingsApp,
/*accessible_name=*/
l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_ACCNAME,
product_name),
/*body_text=*/
l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT,
product_name)))
.AddCustomNextButton(DefaultNextButtonCallback().Then(
base::BindRepeating(&WelcomeTourController::SetCurrentStep,
weak_ptr_factory_.GetMutableWeakPtr(),
@ -281,12 +334,16 @@ WelcomeTourController::GetTutorialDescription() const {
.InSameContext());
// Step 6: Explore app.
// NOTE: The accessible name is the same as the body text.
tutorial_description.steps.emplace_back(
user_education::TutorialDescription::BubbleStep(kExploreAppElementId)
.SetBubbleArrow(user_education::HelpBubbleArrow::kBottomLeft)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT)
.SetBubbleBodyText(IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT)
.SetExtendedProperties(CreateHelpBubbleExtendedProperties(
HelpBubbleId::kWelcomeTourExploreApp))
HelpBubbleId::kWelcomeTourExploreApp,
/*body_text=*/l10n_util::GetStringFUTF8(
IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT,
product_name)))
.InSameContext());
// Step 7: Explore app window.
@ -296,14 +353,27 @@ WelcomeTourController::GetTutorialDescription() const {
}
void WelcomeTourController::OnAccessibilityControllerShutdown() {
if (features::IsWelcomeTourChromeVoxSupported()) {
accessibility_observation_.Reset();
return;
}
MaybeAbortWelcomeTour(welcome_tour_metrics::AbortedReason::kShutdown);
}
void WelcomeTourController::OnAccessibilityStatusChanged() {
if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
MaybeAbortWelcomeTour(
welcome_tour_metrics::AbortedReason::kChromeVoxEnabled);
if (!Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
return;
}
// Record the usage of ChromeVox in Welcome Tour.
if (features::IsWelcomeTourChromeVoxSupported()) {
welcome_tour_metrics::RecordChromeVoxEnabled(
welcome_tour_metrics::ChromeVoxEnabled::kDuringTour);
return;
}
MaybeAbortWelcomeTour(welcome_tour_metrics::AbortedReason::kChromeVoxEnabled);
}
void WelcomeTourController::OnActiveUserSessionChanged(
@ -400,8 +470,9 @@ void WelcomeTourController::MaybeStartWelcomeTour() {
: base::BindOnce(&LaunchExploreAppAsync,
UserEducationPrivateApiKey()));
// Welcome Tour is not supported with ChromeVox enabled.
if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
// Welcome Tour is only conditionally supported with ChromeVox enabled.
if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled() &&
!features::IsWelcomeTourChromeVoxSupported()) {
welcome_tour_metrics::RecordTourPrevented(
welcome_tour_metrics::PreventedReason::kChromeVoxEnabled);
return;
@ -462,6 +533,12 @@ void WelcomeTourController::MaybeAbortWelcomeTour(
}
void WelcomeTourController::OnWelcomeTourStarted() {
if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) {
CHECK(features::IsWelcomeTourChromeVoxSupported());
welcome_tour_metrics::RecordChromeVoxEnabled(
welcome_tour_metrics::ChromeVoxEnabled::kBeforeTour);
}
aborted_reason_ = welcome_tour_metrics::AbortedReason::kUnknown;
accelerator_handler_ = std::make_unique<WelcomeTourAcceleratorHandler>(
base::BindRepeating(&WelcomeTourController::MaybeAbortWelcomeTour,

@ -47,6 +47,7 @@
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chromeos/constants/devicetype.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/user_education/common/tutorial_description.h"
@ -54,6 +55,8 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/types/event_type.h"
@ -68,15 +71,18 @@ namespace ash {
namespace {
// Aliases.
using ::ash::welcome_tour_metrics::ChromeVoxEnabled;
using ::ash::welcome_tour_metrics::PreventedReason;
using ::base::test::RunOnceClosure;
using ::session_manager::SessionState;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Conditional;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::IsTrue;
using ::testing::Matches;
using ::testing::Mock;
@ -110,6 +116,10 @@ auto MoveArgs(T*... out) {
// Matchers --------------------------------------------------------------------
MATCHER_P2(StringFUT8Eq, message_id, sub, "") {
return Matches(l10n_util::GetStringFUTF8(message_id, sub))(arg);
}
MATCHER_P(ElementSpecifierEq, element_specifier, "") {
return absl::visit(base::Overloaded{
[&](const ui::ElementIdentifier& element_id) {
@ -144,6 +154,53 @@ MATCHER_P6(BubbleStep,
&util::GetHelpBubbleBodyIcon(ext_props)->get() == &gfx::kNoneIcon;
}
MATCHER_P7(BubbleStep,
element_specifier,
context_mode,
help_bubble_id,
body_text_id,
body_text_matcher,
arrow,
has_next_button,
"") {
namespace util = user_education_util;
const auto& ext_props = arg.extended_properties();
return arg.step_type() == ui::InteractionSequence::StepType::kShown &&
Matches(ElementSpecifierEq(element_specifier))(arg) &&
arg.context_mode() == context_mode &&
util::GetHelpBubbleId(ext_props) == help_bubble_id &&
arg.body_text_id() == body_text_id && arg.arrow() == arrow &&
Matches(body_text_matcher)(util::GetHelpBubbleBodyText(ext_props)) &&
arg.next_button_callback().is_null() != has_next_button &&
&util::GetHelpBubbleBodyIcon(ext_props)->get() == &gfx::kNoneIcon &&
util::GetHelpBubbleModalType(ext_props) == ui::MODAL_TYPE_SYSTEM;
}
MATCHER_P8(BubbleStep,
element_specifier,
context_mode,
help_bubble_id,
accessible_name_matcher,
body_text_id,
body_text_matcher,
arrow,
has_next_button,
"") {
namespace util = user_education_util;
const auto& ext_props = arg.extended_properties();
return arg.step_type() == ui::InteractionSequence::StepType::kShown &&
Matches(ElementSpecifierEq(element_specifier))(arg) &&
arg.context_mode() == context_mode &&
util::GetHelpBubbleId(ext_props) == help_bubble_id &&
Matches(accessible_name_matcher)(
util::GetHelpBubbleAccessibleName(ext_props)) &&
arg.body_text_id() == body_text_id && arg.arrow() == arrow &&
Matches(body_text_matcher)(util::GetHelpBubbleBodyText(ext_props)) &&
arg.next_button_callback().is_null() != has_next_button &&
&util::GetHelpBubbleBodyIcon(ext_props)->get() == &gfx::kNoneIcon &&
util::GetHelpBubbleModalType(ext_props) == ui::MODAL_TYPE_SYSTEM;
}
MATCHER_P2(HiddenStep, element_specifier, context_mode, "") {
return arg.step_type() == ui::InteractionSequence::StepType::kHidden &&
Matches(ElementSpecifierEq(element_specifier))(arg) &&
@ -303,6 +360,8 @@ TEST_F(WelcomeTourControllerTest, GetTutorialDescription) {
auto* welcome_tour_controller = WelcomeTourController::Get();
ASSERT_TRUE(welcome_tour_controller);
const std::u16string product_name = ui::GetChromeOSDeviceName();
EXPECT_THAT(
welcome_tour_controller->GetTutorialDescription(),
AllOf(
@ -333,36 +392,62 @@ TEST_F(WelcomeTourControllerTest, GetTutorialDescription) {
EventStep(ElementSpecifier(kUnifiedSystemTrayElementName),
ContextMode::kFromPreviousStep,
/*has_name_elements_callback=*/true),
BubbleStep(ElementSpecifier(kHomeButtonElementName),
ContextMode::kAny,
HelpBubbleId::kWelcomeTourHomeButton,
IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT,
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/true),
BubbleStep(ElementSpecifier(kSearchBoxViewElementId),
ContextMode::kAny,
HelpBubbleId::kWelcomeTourSearchBox,
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT,
HelpBubbleArrow::kTopCenter,
/*has_next_button=*/true),
BubbleStep(
ElementSpecifier(kHomeButtonElementName),
ContextMode::kAny, HelpBubbleId::kWelcomeTourHomeButton,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_ACCNAME,
product_name),
IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
StringFUT8Eq(
(chromeos::GetDeviceType() ==
chromeos::DeviceType::kChromebook)
? IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_CHROMEBOOK
: IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_OTHER_DEVICE_TYPES,
product_name),
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/true),
BubbleStep(
ElementSpecifier(kSearchBoxViewElementId),
ContextMode::kAny, HelpBubbleId::kWelcomeTourSearchBox,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_ACCNAME,
product_name),
IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT,
product_name),
HelpBubbleArrow::kTopCenter,
/*has_next_button=*/true),
EventStep(ElementSpecifier(kSearchBoxViewElementId),
ContextMode::kFromPreviousStep,
/*has_name_elements_callback=*/false),
BubbleStep(ElementSpecifier(kSettingsAppElementId),
ContextMode::kFromPreviousStep,
HelpBubbleId::kWelcomeTourSettingsApp,
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT,
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/true),
BubbleStep(
ElementSpecifier(kSettingsAppElementId),
ContextMode::kFromPreviousStep,
HelpBubbleId::kWelcomeTourSettingsApp,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_ACCNAME,
product_name),
IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT,
product_name),
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/true),
EventStep(ElementSpecifier(kSettingsAppElementId),
ContextMode::kFromPreviousStep,
/*has_name_elements_callback=*/false),
BubbleStep(ElementSpecifier(kExploreAppElementId),
ContextMode::kFromPreviousStep,
HelpBubbleId::kWelcomeTourExploreApp,
IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT,
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/false)))));
BubbleStep(
ElementSpecifier(kExploreAppElementId),
ContextMode::kFromPreviousStep,
HelpBubbleId::kWelcomeTourExploreApp,
IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
StringFUT8Eq(
IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT,
product_name),
HelpBubbleArrow::kBottomLeft,
/*has_next_button=*/false)))));
}
// Verifies that the Welcome Tour is started when the primary user session is
@ -515,8 +600,42 @@ TEST_F(WelcomeTourControllerTest, AbortsTourAndPropagatesEvents) {
EXPECT_TRUE(ended_future.Wait());
}
// Verifies the Welcome Tour to be aborted if ChromeVox is enabled during tour.
TEST_F(WelcomeTourControllerTest, AbortTourIfChromeVoxEnabledDuringTour) {
// WelcomeTourControllerChromeVoxTest -------------------------------------
// Base class for tests of the `WelcomeTourController` which are concerned with
// the behaviors when ChromeVox is supported in the Welcome Tour, parameterized
// by whether ChromeVox is supported.
class WelcomeTourControllerChromeVoxTest
: public WelcomeTourControllerTest,
public ::testing::WithParamInterface<
/*is_chromevox_supported=*/std::optional<bool>> {
public:
WelcomeTourControllerChromeVoxTest() {
scoped_feature_list_.InitWithFeatureState(
features::kWelcomeTourChromeVoxSupported, IsChromeVoxSupported());
}
// Returns whether ChromeVox is supported in the Welcome Tour given test
// parameterization.
bool IsChromeVoxSupported() const { return GetParam().value_or(false); }
private:
// Used to conditionally enable ChromeVox support in the Welcome Tour
// given test parameterization.
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
WelcomeTourControllerChromeVoxTest,
/*is_chromevox_supported=*/
::testing::Values(std::make_optional(true),
std::make_optional(false),
std::nullopt));
// Verifies the Welcome Tour is aborted if ChromeVox is not supported but is
// enabled during the tour.
TEST_P(WelcomeTourControllerChromeVoxTest,
MaybeAbortTourIfChromeVoxEnabledDuringTour) {
// Start the Welcome Tour by logging in the primary user for the first time.
const auto primary_account_id = AccountId::FromUserEmail("primary@test");
SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());
@ -527,33 +646,48 @@ TEST_F(WelcomeTourControllerTest, AbortTourIfChromeVoxEnabledDuringTour) {
observation{&observer};
observation.Observe(WelcomeTourController::Get());
// Satisfy `ended_future` when an end event is received.
base::test::TestFuture<void> ended_future;
EXPECT_CALL(observer, OnWelcomeTourEnded)
.WillOnce(RunOnceClosure(ended_future.GetCallback()));
// Expect the Welcome Tour to be aborted when enabling ChromeVox during tour.
// The Welcome Tour is only expected to abort when ChromeVox is enabled if
// ChromeVox is not supported.
const bool expect_abort = !IsChromeVoxSupported();
EXPECT_CALL(
*user_education_delegate(),
AbortTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour)));
AbortTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour)))
.Times(expect_abort ? 1 : 0);
// Expect an attempt to launch the Explore app when the tour is aborted.
// Expect the Welcome Tour to end only if it is expected to abort.
EXPECT_CALL(observer, OnWelcomeTourEnded).Times(expect_abort ? 1 : 0);
// Expect an attempt to launch the Explore app only if the Welcome Tour is
// aborted.
EXPECT_CALL(*user_education_delegate(),
LaunchSystemWebAppAsync(
Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
Eq(apps::LaunchSource::kFromWelcomeTour),
Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())));
Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
.Times(expect_abort ? 1 : 0);
base::HistogramTester histogram_tester;
auto* const a11y_controller = Shell::Get()->accessibility_controller();
a11y_controller->SetSpokenFeedbackEnabled(true, A11Y_NOTIFICATION_NONE);
Mock::VerifyAndClearExpectations(user_education_delegate());
EXPECT_TRUE(a11y_controller->spoken_feedback().enabled());
// Verify histograms.
EXPECT_THAT(
histogram_tester.GetAllSamples("Ash.WelcomeTour.ChromeVoxEnabled.When"),
Conditional(IsChromeVoxSupported(),
BucketsAre(base::Bucket(ChromeVoxEnabled::kBeforeTour, 0),
base::Bucket(ChromeVoxEnabled::kDuringTour, 1)),
IsEmpty()));
}
// Checks that the Welcome Tour should NOT start if ChromeVox is enabled.
TEST_F(WelcomeTourControllerTest, PreventTourFromStartingIfChromeVoxEnabled) {
// Verifies the Welcome Tour is prevented from starting if ChromeVox is not
// supported but is enabled.
TEST_P(WelcomeTourControllerChromeVoxTest,
MaybePreventTourFromStartingIfChromeVoxEnabled) {
const auto primary_account_id = AccountId::FromUserEmail("primary@test");
base::HistogramTester histogram_tester;
TestSessionControllerClient* const session = GetSessionControllerClient();
session->AddUserSession(
primary_account_id.GetUserEmail(), user_manager::UserType::kRegular,
@ -567,17 +701,33 @@ TEST_F(WelcomeTourControllerTest, PreventTourFromStartingIfChromeVoxEnabled) {
EXPECT_TRUE(a11y_controller->spoken_feedback().enabled());
// Start the Welcome Tour by activating the user session. Expect that the
// Welcome Tour is NOT registered or started but that an attempt is made to
// launch the Explore app.
EXPECT_CALL(*user_education_delegate(), RegisterTutorial).Times(0);
EXPECT_CALL(*user_education_delegate(), StartTutorial).Times(0);
// Welcome Tour is NOT registered or started when ChromeVox is enabled if
// ChromeVox is not supported.
const bool expect_prevent = !IsChromeVoxSupported();
EXPECT_CALL(*user_education_delegate(), RegisterTutorial)
.Times(expect_prevent ? 0 : 1);
EXPECT_CALL(*user_education_delegate(), StartTutorial)
.Times(expect_prevent ? 0 : 1);
// Expect an attempt to launch the Explore app only if the Welcome Tour is
// prevented.
EXPECT_CALL(*user_education_delegate(),
LaunchSystemWebAppAsync(
Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
Eq(apps::LaunchSource::kFromWelcomeTour),
Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())));
Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
.Times(expect_prevent ? 1 : 0);
session->SetSessionState(SessionState::ACTIVE);
Mock::VerifyAndClearExpectations(user_education_delegate());
// Verify histograms.
EXPECT_THAT(
histogram_tester.GetAllSamples("Ash.WelcomeTour.ChromeVoxEnabled.When"),
Conditional(IsChromeVoxSupported(),
BucketsAre(base::Bucket(ChromeVoxEnabled::kBeforeTour, 1),
base::Bucket(ChromeVoxEnabled::kDuringTour, 0)),
IsEmpty()));
}
// WelcomeTourControllerCounterfactualTest -------------------------------------

@ -4,6 +4,7 @@
#include "ash/user_education/welcome_tour/welcome_tour_dialog.h"
#include <string>
#include <utility>
#include "ash/ash_element_identifiers.h"
@ -16,6 +17,7 @@
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
@ -66,6 +68,8 @@ WelcomeTourDialog::WelcomeTourDialog(base::OnceClosure accept_callback,
CHECK_EQ(g_instance, nullptr);
g_instance = this;
const std::u16string product_name = ui::GetChromeOSDeviceName();
views::Builder<SystemDialogDelegateView>(this)
.SetAcceptButtonText(l10n_util::GetStringUTF16(
IDS_ASH_WELCOME_TOUR_DIALOG_ACCEPT_BUTTON_TEXT))
@ -74,12 +78,12 @@ WelcomeTourDialog::WelcomeTourDialog(base::OnceClosure accept_callback,
IDS_ASH_WELCOME_TOUR_DIALOG_CANCEL_BUTTON_TEXT))
.SetCancelCallback(std::move(cancel_callback))
.SetCloseCallback(std::move(close_callback))
.SetDescription(l10n_util::GetStringUTF16(
IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT))
.SetDescription(l10n_util::GetStringFUTF16(
IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT, product_name))
.SetModalType(ui::ModalType::MODAL_TYPE_SYSTEM)
.SetProperty(views::kElementIdentifierKey, kWelcomeTourDialogElementId)
.SetTitleText(
l10n_util::GetStringUTF16(IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT))
.SetTitleText(l10n_util::GetStringFUTF16(
IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT, product_name))
.SetTopContentView(views::Builder<views::ImageView>()
.SetImage(ui::ResourceBundle::GetSharedInstance()
.GetThemedLottieImageNamed(

@ -38,7 +38,7 @@ TEST_F(WelcomeTourDialogPixelTest, Appearance) {
// Take a screenshot of the Welcome Tour dialog.
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"welcome_tour_dialog",
/*revision_number=*/2, WelcomeTourDialog::Get()));
/*revision_number=*/3, WelcomeTourDialog::Get()));
}
} // namespace ash

@ -28,6 +28,12 @@ PrefService* GetLastActiveUserPrefService() {
} // namespace
void RecordChromeVoxEnabled(ChromeVoxEnabled when) {
CHECK(features::IsWelcomeTourEnabled());
base::UmaHistogramEnumeration("Ash.WelcomeTour.ChromeVoxEnabled.When", when);
}
void RecordInteraction(Interaction interaction) {
CHECK(features::IsWelcomeTourEnabled());

@ -32,6 +32,16 @@ enum class AbortedReason {
kMaxValue = kShutdown,
};
// Enumeration of when ChromeVox is enabled in the Welcome Tour. These values
// are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class ChromeVoxEnabled {
kMinValue = 0,
kBeforeTour = kMinValue,
kDuringTour = 1,
kMaxValue = kDuringTour,
};
// Enumeration of reasons the Welcome Tour may be prevented. These values are
// persisted to logs. Entries should not be renumbered and numeric values should
// never be reused. Be sure to update `kAllPreventedReasonsSet` accordingly.
@ -106,6 +116,9 @@ static constexpr auto kAllInteractionsSet =
// Utilities -------------------------------------------------------------------
// Record the usage of ChromeVox in the Welcome Tour.
ASH_EXPORT void RecordChromeVoxEnabled(ChromeVoxEnabled when);
// Record that a given `interaction` has occurred.
ASH_EXPORT void RecordInteraction(Interaction interaction);

@ -22,9 +22,11 @@
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "chromeos/constants/devicetype.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/test/browser_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/test/event_generator.h"
@ -171,19 +173,22 @@ class WelcomeTourInteractiveUiTest : public InteractiveBrowserTest {
// Returns a builder for an interaction step that checks the dialog
// description.
[[nodiscard]] static auto CheckDialogDescription() {
const std::u16string product_name = ui::GetChromeOSDeviceName();
return CheckViewProperty(
ash::SystemDialogDelegateView::kDescriptionTextIdForTesting,
&views::Label::GetText,
l10n_util::GetStringUTF16(
IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT));
l10n_util::GetStringFUTF16(IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT,
product_name));
}
// Returns a builder for an interaction step that checks the dialog title.
[[nodiscard]] static auto CheckDialogTitle() {
const std::u16string product_name = ui::GetChromeOSDeviceName();
return CheckViewProperty(
ash::SystemDialogDelegateView::kTitleTextIdForTesting,
&views::Label::GetText,
l10n_util::GetStringUTF16(IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT));
l10n_util::GetStringFUTF16(IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT,
product_name));
}
// Returns a builder for an interaction step that checks that the anchor of a
@ -199,11 +204,11 @@ class WelcomeTourInteractiveUiTest : public InteractiveBrowserTest {
}
// Returns a builder for an interaction step that checks that the body text of
// a help bubble matches the specified `message_id`.
[[nodiscard]] static auto CheckHelpBubbleBodyText(int message_id) {
// a help bubble matches the specified `body_text`.
[[nodiscard]] static auto CheckHelpBubbleBodyText(
const std::u16string& body_text) {
return CheckViewProperty(ash::HelpBubbleViewAsh::kBodyTextIdForTesting,
&views::Label::GetText,
l10n_util::GetStringUTF16(message_id));
&views::Label::GetText, body_text);
}
// Returns a builder for an interaction step that checks whether the help
@ -242,6 +247,8 @@ class WelcomeTourInteractiveUiTest : public InteractiveBrowserTest {
// An interactive UI test that exercises the entire Welcome Tour.
IN_PROC_BROWSER_TEST_F(WelcomeTourInteractiveUiTest, WelcomeTour) {
const std::u16string product_name = ui::GetChromeOSDeviceName();
RunTestSequence(
// Step 0: Dialog.
InAnyContext(WaitForDialogVisibility(true)),
@ -252,66 +259,71 @@ IN_PROC_BROWSER_TEST_F(WelcomeTourInteractiveUiTest, WelcomeTour) {
// Step 1: Shelf.
InAnyContext(WaitForHelpBubble()),
InSameContext(Steps(
CheckHelpBubbleAnchor(ash::kShelfViewElementId),
CheckHelpBubbleBodyText(IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_BODY_TEXT),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
InSameContext(
Steps(CheckHelpBubbleAnchor(ash::kShelfViewElementId),
CheckHelpBubbleBodyText(l10n_util::GetStringUTF16(
IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_BODY_TEXT)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 2: Status area.
InAnyContext(WaitForHelpBubble()),
InSameContext(
Steps(CheckHelpBubbleAnchor(ash::kUnifiedSystemTrayElementId),
CheckHelpBubbleBodyText(
IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT),
CheckHelpBubbleBodyText(l10n_util::GetStringUTF16(
IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 3: Home button.
InAnyContext(WaitForHelpBubble()),
InSameContext(
Steps(CheckHelpBubbleAnchor(ash::kHomeButtonElementId),
CheckHelpBubbleBodyText(
IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
InSameContext(Steps(
CheckHelpBubbleAnchor(ash::kHomeButtonElementId),
CheckHelpBubbleBodyText(l10n_util::GetStringFUTF16(
(chromeos::GetDeviceType() == chromeos::DeviceType::kChromebook)
? IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_CHROMEBOOK
: IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_BODY_TEXT_OTHER_DEVICE_TYPES,
product_name)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 4: Search box.
InAnyContext(WaitForHelpBubble()),
InSameContext(
Steps(CheckAppListBubbleVisibility(true),
CheckHelpBubbleAnchor(ash::kSearchBoxViewElementId),
CheckHelpBubbleBodyText(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
InSameContext(Steps(
CheckAppListBubbleVisibility(true),
CheckHelpBubbleAnchor(ash::kSearchBoxViewElementId),
CheckHelpBubbleBodyText(l10n_util::GetStringFUTF16(
IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_BODY_TEXT, product_name)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 5: Settings app.
InAnyContext(WaitForHelpBubble()),
InSameContext(
Steps(CheckAppListBubbleVisibility(true),
CheckHelpBubbleAnchor(ash::kSettingsAppElementId),
CheckHelpBubbleBodyText(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT),
CheckHelpBubbleBodyText(l10n_util::GetStringFUTF16(
IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT,
product_name)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 6: Explore app.
InAnyContext(WaitForHelpBubble()),
InSameContext(
Steps(CheckAppListBubbleVisibility(true),
CheckHelpBubbleAnchor(ash::kExploreAppElementId),
CheckHelpBubbleBodyText(
IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(
IDS_ASH_WELCOME_TOUR_COMPLETE_BUTTON_TEXT),
PressHelpBubbleDefaultButton(), FlushEvents())),
InSameContext(Steps(
CheckAppListBubbleVisibility(true),
CheckHelpBubbleAnchor(ash::kExploreAppElementId),
CheckHelpBubbleBodyText(l10n_util::GetStringFUTF16(
IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT, product_name)),
CheckHelpBubbleDefaultButtonFocus(true),
CheckHelpBubbleDefaultButtonText(
IDS_ASH_WELCOME_TOUR_COMPLETE_BUTTON_TEXT),
PressHelpBubbleDefaultButton(), FlushEvents())),
// Step 7: Explore app window.
InAnyContext(WaitForBrowser()),

@ -11268,6 +11268,7 @@ if (!is_android && !is_chromeos_device) {
"//chromeos/ash/components/standalone_browser",
"//components/app_constants",
"//device/udev_linux:test_support",
"//ui/chromeos",
"//ui/events/devices",
"//ui/events/devices:test_support",
]

@ -76,6 +76,11 @@ chromium-metrics-reviews@google.com.
<int value="5" label="kShutdown"/>
</enum>
<enum name="WelcomeTourChromeVoxEnabled">
<int value="0" label="kBeforeTour"/>
<int value="1" label="kDuringTour"/>
</enum>
<enum name="WelcomeTourInteraction">
<int value="0" label="kFilesApp"/>
<int value="1" label="kLauncher"/>

@ -182,6 +182,16 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="Ash.WelcomeTour.ChromeVoxEnabled.When"
enum="WelcomeTourChromeVoxEnabled" expires_after="2025-02-01">
<owner>wutao@chromium.org</owner>
<owner>teresachow@google.com</owner>
<summary>
Records when ChromeVox is enabled in the Welcome Tour. Only logged when the
feature flag is turned on and the tour is shown.
</summary>
</histogram>
<histogram name="Ash.WelcomeTour.Prevented.Reason"
enum="WelcomeTourPreventedReason" expires_after="2024-08-04">
<owner>dmblack@google.com</owner>