chromium/chrome/browser/ui/ash/user_education/welcome_tour/welcome_tour_interactive_uitest.cc

// 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/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pill_button.h"
#include "ash/style/system_dialog_delegate_view.h"
#include "ash/user_education/views/help_bubble_view_ash.h"
#include "ash/user_education/welcome_tour/welcome_tour_controller.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/app_restore/full_restore_app_launch_handler.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#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"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

namespace {

// Aliases.
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Matches;
using ::testing::Property;

using TestVariantsParam = std::tuple<
    /*is_apps_collections_enabled=*/bool,
    /*is_welcome_tour_v2_enabled=*/bool,
    /*is_welcome_tour_counterfactaully_enabled=*/bool>;

// Matchers --------------------------------------------------------------------

MATCHER_P(ElementIdentifier, matcher, "") {
  return Matches(matcher)(arg->GetProperty(views::kElementIdentifierKey));
}

MATCHER_P(RootWindow, matcher, "") {
  return Matches(matcher)(arg->GetWidget()->GetNativeWindow()->GetRootWindow());
}

// Helpers ---------------------------------------------------------------------

bool IsAppsCollectionsEnabled(TestVariantsParam param) {
  return std::get<0>(param);
}

bool IsWelcomeTourV2Enabled(TestVariantsParam param) {
  return std::get<1>(param);
}

bool IsWelcomeTourCounterfactuallyEnabled(TestVariantsParam param) {
  return std::get<2>(param);
}

std::string GenerateTestSuffix(
    const testing::TestParamInfo<TestVariantsParam>& info) {
  return base::StrCat(
      {IsWelcomeTourV2Enabled(info.param) ? "V2" : "V1", "_",
       IsWelcomeTourCounterfactuallyEnabled(info.param) ? "Counterfactual_"
                                                        : "",
       IsAppsCollectionsEnabled(info.param) ? "AppsCollectionsEnabled"
                                            : "AppsCollectionsDisabled"});
}

}  // namespace

// WelcomeTourInteractiveUiTest ------------------------------------------------

// Base class for interactive UI tests of the Welcome Tour in Ash.
class WelcomeTourInteractiveUiTest
    : public InteractiveBrowserTest,
      public testing::WithParamInterface<TestVariantsParam> {
 public:
  WelcomeTourInteractiveUiTest() {
    // NOTE: These tests are not concerned with user eligibility, so explicitly
    // force user eligibility for the Welcome Tour.
    // Only one of `kWelcomeTourHoldbackArm`, `kWelcomeTourCounterfactualArm`
    // and `kWelcomeTourV2` can be enabled at a time.
    scoped_feature_list_.InitWithFeatureStates(
        {{ash::features::kWelcomeTour, true},
         {ash::features::kWelcomeTourForceUserEligibility, true},
         {ash::features::kWelcomeTourV2,
          IsWelcomeTourV2Enabled() && !IsWelcomeTourCounterfactuallyEnabled()},
         {ash::features::kWelcomeTourCounterfactualArm,
          IsWelcomeTourCounterfactuallyEnabled()},
         {ash::features::kWelcomeTourHoldbackArm, false},
         {app_list_features::kAppsCollections, IsAppsCollectionsEnabled()},
         {app_list_features::kForceShowAppsCollections,
          IsAppsCollectionsEnabled()}});

    // TODO(http://b/277091006): Remove after preventing app launches.
    // Prevent the browser from launching as it is not needed to fully exercise
    // the Welcome Tour and can only add flakiness. Eventually, logic will be
    // added to production code to prevent app launches while the Welcome Tour
    // is in progress.
    set_launch_browser_for_testing(nullptr);
  }

  // InteractiveBrowserTest:
  void SetUpOnMainThread() override {
    InteractiveBrowserTest::SetUpOnMainThread();

    // Install system apps.
    // NOTE: This test requires the "Help" and "Settings" apps to be installed.
    Profile* const profile = ProfileManager::GetActiveUserProfile();
    web_app::test::WaitUntilReady(web_app::WebAppProvider::GetForTest(profile));
    ash::SystemWebAppManager::GetForTest(profile)
        ->InstallSystemAppsForTesting();
    AppListClientImpl::GetInstance()->UpdateProfile();

    // Ensure that the widget context for the test interaction sequence matches
    // the initial element context used to start the Welcome Tour.
    SetContextWidget(
        views::ElementTrackerViews::GetInstance()->GetWidgetForContext(
            ash::WelcomeTourController::Get()->GetInitialElementContext()));
  }

  // Returns whether the AppsCollections feature is enabled in the Welcome Tour
  // given test parameterization.
  bool IsAppsCollectionsEnabled() const {
    return ::IsAppsCollectionsEnabled(GetParam());
  }

  // Returns whether the WelcomeTourV2 feature is enabled given test
  // parameterization.
  bool IsWelcomeTourV2Enabled() const {
    return ::IsWelcomeTourV2Enabled(GetParam());
  }

  // Returns whether the WelcomeTour feature is counterfactually enabled given
  // test parameterization.
  bool IsWelcomeTourCounterfactuallyEnabled() const {
    return ::IsWelcomeTourCounterfactuallyEnabled(GetParam());
  }

  // Returns a builder for an interaction step that waits for the app list
  // bubble to hide.
  [[nodiscard]] static auto WaitForAppListBubbleToHide() {
    return WaitForHide(ash::HelpBubbleViewAsh::kHelpBubbleElementIdForTesting);
  }

  // Returns a builder for an interaction step that waits for the browser.
  [[nodiscard]] static auto WaitForBrowser() {
    return WaitForShow(kBrowserViewElementId);
  }

  // Returns a builder for an interaction step that waits for the dialog to have
  // the expected visibility.
  [[nodiscard]] static auto WaitForDialogVisibility(bool visible) {
    return visible ? WaitForShow(ash::kWelcomeTourDialogElementId)
                   : WaitForHide(ash::kWelcomeTourDialogElementId);
  }

  // Returns a builder for an interaction step that waits for a help bubble.
  [[nodiscard]] static auto WaitForHelpBubble() {
    return WaitForShow(ash::HelpBubbleViewAsh::kHelpBubbleElementIdForTesting);
  }

  // Returns a builder for an interaction step that waits for login user view.
  [[nodiscard]] static auto WaitForLoginUserView() {
    return WaitForShow(ash::kLoginUserViewElementId);
  }

  // Returns a builder for an interaction step that checks the visibility of
  // app list bubble.
  [[nodiscard]] static auto CheckAppListBubbleVisibility(bool visible) {
    return CheckViewProperty(ash::kAppListBubbleViewElementId,
                             &views::View::GetVisible, visible);
  }

  // Returns a builder for an interaction step that checks the browser is
  // for a web app associated with the specified `app_id`.
  [[nodiscard]] static auto CheckBrowserIsForWebApp(
      const webapps::AppId& app_id) {
    return CheckView(kBrowserViewElementId,
                     [app_id](BrowserView* browser_view) {
                       return web_app::AppBrowserController::IsForWebApp(
                           browser_view->browser(), app_id);
                     });
  }

  // Returns a builder for an interaction step that checks whether the dialog
  // accept button is focused.
  [[nodiscard]] static auto CheckDialogAcceptButtonFocus(bool focused) {
    return CheckViewProperty(
        ash::SystemDialogDelegateView::kAcceptButtonIdForTesting,
        &views::MdTextButton::HasFocus, focused);
  }

  // Returns a builder for an interaction step that checks the dialog accept
  // button text.
  [[nodiscard]] static auto CheckDialogAcceptButtonText() {
    return CheckViewProperty(
        ash::SystemDialogDelegateView::kAcceptButtonIdForTesting,
        &ash::PillButton::GetText,
        l10n_util::GetStringUTF16(
            IDS_ASH_WELCOME_TOUR_DIALOG_ACCEPT_BUTTON_TEXT));
  }

  // Returns a builder for an interaction step that checks the dialog cancel
  // button text.
  [[nodiscard]] static auto CheckDialogCancelButtonText() {
    return CheckViewProperty(
        ash::SystemDialogDelegateView::kCancelButtonIdForTesting,
        &ash::PillButton::GetText,
        l10n_util::GetStringUTF16(
            IDS_ASH_WELCOME_TOUR_DIALOG_CANCEL_BUTTON_TEXT));
  }

  // Returns a builder for an interaction step that checks the dialog
  // description.
  [[nodiscard]] static auto CheckDialogDescription(int message_id) {
    const std::u16string product_name = ui::GetChromeOSDeviceName();
    return CheckViewProperty(
        ash::SystemDialogDelegateView::kDescriptionTextIdForTesting,
        &views::Label::GetText,
        l10n_util::GetStringFUTF16(message_id, product_name));
  }

  // Returns a builder for an interaction step that checks the dialog title.
  [[nodiscard]] static auto CheckDialogTitle(int message_id) {
    const std::u16string product_name = ui::GetChromeOSDeviceName();
    return CheckViewProperty(
        ash::SystemDialogDelegateView::kTitleTextIdForTesting,
        &views::Label::GetText,
        l10n_util::GetStringFUTF16(message_id, product_name));
  }

  // Returns a builder for an interaction step that checks that the anchor of a
  // help bubble (a) matches the specified `element_id`, and (b) is contained
  // within the primary root window.
  [[nodiscard]] static auto CheckHelpBubbleAnchor(
      ui::ElementIdentifier element_id) {
    return CheckViewProperty<ash::HelpBubbleViewAsh, views::View*>(
        ash::HelpBubbleViewAsh::kHelpBubbleElementIdForTesting,
        &ash::HelpBubbleViewAsh::GetAnchorView,
        AllOf(ElementIdentifier(Eq(element_id)),
              RootWindow(Eq(ash::Shell::GetPrimaryRootWindow()))));
  }

  // Returns a builder for an interaction step that checks that the body text of
  // 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, body_text);
  }

  // Returns a builder for an interaction step that checks whether the help
  // bubble default button is focused.
  [[nodiscard]] static auto CheckHelpBubbleDefaultButtonFocus(bool focused) {
    return CheckViewProperty(ash::HelpBubbleViewAsh::kDefaultButtonIdForTesting,
                             &views::MdTextButton::HasFocus, focused);
  }

  // Returns a builder for an interaction step that checks that the default
  // button text of a help bubble matches the specified `message_id`.
  [[nodiscard]] static auto CheckHelpBubbleDefaultButtonText(int message_id) {
    return CheckViewProperty(ash::HelpBubbleViewAsh::kDefaultButtonIdForTesting,
                             &views::LabelButton::GetText,
                             l10n_util::GetStringUTF16(message_id));
  }

  // Returns a builder for an interaction step that presses the dialog accept
  // button.
  [[nodiscard]] auto PressDialogAcceptButton() {
    return PressButton(
        ash::SystemDialogDelegateView::kAcceptButtonIdForTesting);
  }

  // Returns a builder for an interaction step that presses the default button
  // of a help bubble.
  [[nodiscard]] auto PressHelpBubbleDefaultButton() {
    return PressButton(ash::HelpBubbleViewAsh::kDefaultButtonIdForTesting);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    WelcomeTourInteractiveUiTest,
    testing::Combine(
        /*is_apps_collections_enabled=*/testing::Bool(),
        /*is_welcome_tour_v2_enabled=*/testing::Bool(),
        /*is_welcome_tour_counterfactually_enabled=*/testing::Bool()),
    &GenerateTestSuffix);

// Tests -----------------------------------------------------------------------

// An interactive UI test that exercises the entire Welcome Tour.
IN_PROC_BROWSER_TEST_P(WelcomeTourInteractiveUiTest, WelcomeTour) {
  const std::u16string product_name = ui::GetChromeOSDeviceName();

  RunTestSequence(
      // Step 0: Dialog.
      InAnyContext(WaitForDialogVisibility(true)),
      InSameContext(
          Steps(CheckDialogAcceptButtonFocus(true),
                CheckDialogAcceptButtonText(), CheckDialogCancelButtonText(),
                CheckDialogDescription(
                    ash::features::IsWelcomeTourV2Enabled()
                        ? IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT_V2
                        : IDS_ASH_WELCOME_TOUR_DIALOG_DESCRIPTION_TEXT),
                CheckDialogTitle(ash::features::IsWelcomeTourV2Enabled()
                                     ? IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT_V2
                                     : IDS_ASH_WELCOME_TOUR_DIALOG_TITLE_TEXT),
                PressDialogAcceptButton())),

      // Step 1: Shelf.
      InAnyContext(WaitForHelpBubble()),
      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())),

      // Step 2: Status area.
      InAnyContext(WaitForHelpBubble()),
      InSameContext(
          Steps(CheckHelpBubbleAnchor(ash::kUnifiedSystemTrayElementId),
                CheckHelpBubbleBodyText(l10n_util::GetStringUTF16(
                    IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT)),
                CheckHelpBubbleDefaultButtonFocus(true),
                CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
                PressHelpBubbleDefaultButton())),

      // Step 3: Home button.
      InAnyContext(WaitForHelpBubble()),
      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())),

      // Step 4: Search box.
      InAnyContext(WaitForHelpBubble()),
      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())),

      // Step 5 in V2: Files app.
      If([&] { return ash::features::IsWelcomeTourV2Enabled(); },
         InAnyContext(
             Steps(WaitForHelpBubble(), CheckAppListBubbleVisibility(true),
                   CheckHelpBubbleAnchor(ash::kFilesAppElementId),
                   CheckHelpBubbleBodyText(l10n_util::GetStringUTF16(
                       IDS_ASH_WELCOME_TOUR_FILES_APP_BUBBLE_BODY_TEXT)),
                   CheckHelpBubbleDefaultButtonFocus(true),
                   CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
                   PressHelpBubbleDefaultButton()))),

      // Step 5 in V1 and step 6 in V2: Settings app.
      InAnyContext(WaitForHelpBubble()),
      InSameContext(
          Steps(CheckAppListBubbleVisibility(true),
                CheckHelpBubbleAnchor(ash::kSettingsAppElementId),
                CheckHelpBubbleBodyText(l10n_util::GetStringFUTF16(
                    IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_BODY_TEXT,
                    product_name)),
                CheckHelpBubbleDefaultButtonFocus(true),
                CheckHelpBubbleDefaultButtonText(IDS_TUTORIAL_NEXT_BUTTON),
                PressHelpBubbleDefaultButton())),

      // Step 6 in V1 and step 7 in V2: Explore app.
      InAnyContext(WaitForHelpBubble()),
      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())),

      // Step 7 in V1 and step 8 in V2: Explore app window.
      InAnyContext(WaitForBrowser()),
      InSameContext(Steps(WaitForAppListBubbleToHide(),
                          CheckBrowserIsForWebApp(web_app::kHelpAppId))));
}

// An interactive UI test that locks the screen during the Welcome Tour.
IN_PROC_BROWSER_TEST_P(WelcomeTourInteractiveUiTest,
                       LockScreenDuringWelcomeTour) {
  RunTestSequence(
      // Wait for the Welcome Tour dialog to show.
      InAnyContext(WaitForDialogVisibility(true)),

      InSameContext(Steps(
          // Lock screen through accelerator.
          Do([]() {
            ui::test::EventGenerator(ash::Shell::GetPrimaryRootWindow())
                .PressAndReleaseKeyAndModifierKeys(ui::KeyboardCode::VKEY_L,
                                                   ui::EF_COMMAND_DOWN);
          }),

          // Wait for the Welcome Tour dialog to hide.
          WaitForDialogVisibility(false))),

      // Wait for the login user view to show.
      InAnyContext(WaitForLoginUserView()));
}