chromium/ash/user_education/welcome_tour/welcome_tour_controller_unittest.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/user_education/welcome_tour/welcome_tour_controller.h"

#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/accelerators/accelerator_lookup.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/accelerator_actions.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/system_notification_builder.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/power/battery_notification.h"
#include "ash/test/test_widget_builder.h"
#include "ash/user_education/mock_user_education_delegate.h"
#include "ash/user_education/user_education_ash_test_base.h"
#include "ash/user_education/user_education_types.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/welcome_tour/mock_welcome_tour_controller_observer.h"
#include "ash/user_education/welcome_tour/welcome_tour_accelerator_handler.h"
#include "ash/user_education/welcome_tour/welcome_tour_controller_observer.h"
#include "ash/user_education/welcome_tour/welcome_tour_dialog.h"
#include "ash/user_education/welcome_tour/welcome_tour_metrics.h"
#include "ash/user_education/welcome_tour/welcome_tour_test_util.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/functional/callback.h"
#include "base/functional/overloaded.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_move_support.h"
#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"
#include "components/user_manager/user_type.h"
#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/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/types/event_type.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"

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;
using ::testing::NotNull;
using ::testing::Pair;
using ::testing::Property;
using ::testing::Return;
using ::testing::ReturnRefOfCopy;
using ::testing::StrictMock;
using ::user_education::HelpBubbleArrow;
using ::user_education::TutorialDescription;
using ::views::test::WidgetDestroyedWaiter;

using AcceleratorDetails = AcceleratorLookup::AcceleratorDetails;
using ContextMode = TutorialDescription::ContextMode;
using ElementSpecifier = TutorialDescription::ElementSpecifier;

// Strings.
constexpr char16_t kTotalStepsV1[] = u"5";
constexpr char16_t kTotalStepsV2[] = u"6";

// Actions ---------------------------------------------------------------------

// TODO(http://b/277094923): Try to promote to //base/test/gmock_move_support.h.
// Existing support is limited in that the gMock framework provides const-only
// access to `args` for all except the last action. This action lessens the
// effect of that limitation by supporting multiple moves at a time.
template <size_t... I, typename... T>
auto MoveArgs(T*... out) {
  return [out...](auto&&... args) {
    // Assigns the Ith-indexed value from `args` to `out` parameters by move.
    ([&]() { *out = std::move(std::get<I>(std::tie(args...))); }(), ...);
  };
}

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

MATCHER_P(StringUTF8Eq, message_id, "") {
  return Matches(l10n_util::GetStringUTF8(message_id))(arg);
}

MATCHER_P2(StringFUTF8Eq, message_id, sub, "") {
  return Matches(l10n_util::GetStringFUTF8(message_id, sub))(arg);
}

MATCHER_P3(StringFUTF8Eq, message_id, sub1, sub2, "") {
  return Matches(l10n_util::GetStringFUTF8(message_id, sub1, sub2))(arg);
}

MATCHER_P4(StringFUTF8Eq, message_id, sub1, sub2, sub3, "") {
  return Matches(l10n_util::GetStringFUTF8(message_id, sub1, sub2, sub3))(arg);
}

MATCHER_P(ElementSpecifierEq, element_specifier, "") {
  return std::visit(base::Overloaded{
                        [&](const ui::ElementIdentifier& element_id) {
                          return arg.element_id() == element_id &&
                                 arg.element_name().empty();
                        },
                        [&](const std::string& element_name) {
                          return arg.element_name() == element_name &&
                                 arg.element_id() == ui::ElementIdentifier();
                        },
                    },
                    element_specifier);
}

MATCHER_P6(BubbleStep,
           element_specifier,
           context_mode,
           help_bubble_id,
           body_text_id,
           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 &&
         arg.next_button_callback().is_null() != has_next_button &&
         util::GetHelpBubbleModalType(ext_props) ==
             ui::mojom::ModalType::kSystem &&
         &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::mojom::ModalType::kSystem;
}

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::mojom::ModalType::kSystem;
}

MATCHER_P2(HiddenStep, element_specifier, context_mode, "") {
  return arg.step_type() == ui::InteractionSequence::StepType::kHidden &&
         Matches(ElementSpecifierEq(element_specifier))(arg) &&
         arg.context_mode() == context_mode;
}

MATCHER_P3(EventStep,
           element_specifier,
           context_mode,
           has_name_elements_callback,
           "") {
  return arg.step_type() == ui::InteractionSequence::StepType::kCustomEvent &&
         Matches(ElementSpecifierEq(element_specifier))(arg) &&
         arg.context_mode() == context_mode &&
         arg.name_elements_callback().is_null() != has_name_elements_callback;
}

MATCHER_P2(ShownStep, element_specifier, context_mode, "") {
  return arg.step_type() == ui::InteractionSequence::StepType::kShown &&
         Matches(ElementSpecifierEq(element_specifier))(arg) &&
         arg.context_mode() == context_mode;
}

MATCHER_P(StepEq, step, "") {
  return std::make_tuple(arg.arrow(), arg.body_text_id(), arg.context_mode(),
                         arg.element_id(), arg.element_name(), arg.event_type(),
                         arg.extended_properties(), arg.must_be_visible(),
                         arg.must_remain_visible(),
                         arg.name_elements_callback().is_null(),
                         arg.next_button_callback().is_null(), arg.step_type(),
                         arg.subsequence_mode(), arg.title_text_id(),
                         arg.transition_only_on_event()) ==
         std::make_tuple(step.arrow(), step.body_text_id(), step.context_mode(),
                         step.element_id(), step.element_name(),
                         step.event_type(), step.extended_properties(),
                         step.must_be_visible(), step.must_remain_visible(),
                         step.name_elements_callback().is_null(),
                         step.next_button_callback().is_null(),
                         step.step_type(), step.subsequence_mode(),
                         step.title_text_id(), step.transition_only_on_event());
}

MATCHER_P(TutorialDescriptionEqInternal, tutorial_description, "") {
  return std::tie(arg.can_be_restarted, arg.complete_button_text_id) ==
             std::tie(tutorial_description->data.can_be_restarted,
                      tutorial_description->data.complete_button_text_id) &&
         base::ranges::equal(
             arg.steps, tutorial_description->data.steps,
             [](auto& a, auto& b) { return Matches(StepEq(a))(b); });
}

// NOTE: Exists only because `user_education::TutorialDescription` is move-only.
auto TutorialDescriptionEq(
    user_education::TutorialDescription tutorial_description) {
  return TutorialDescriptionEqInternal(
      base::MakeRefCounted<
          base::RefCountedData<user_education::TutorialDescription>>(
          std::move(tutorial_description)));
}

// MockPretargetEventHandler ---------------------------------------------------

// A mock pre-target event handler to expose the received events.
class MockPretargetEventHandler : public ui::EventHandler {
 public:
  MockPretargetEventHandler() {
    Shell::Get()->AddPreTargetHandler(this, ui::EventTarget::Priority::kSystem);
  }

  ~MockPretargetEventHandler() override {
    Shell::Get()->RemovePreTargetHandler(this);
  }

  // ui::EventHandler:
  MOCK_METHOD(void, OnKeyEvent, (ui::KeyEvent*), (override));
};

// MockView --------------------------------------------------------------------

// A mocked view to expose received events.
class MockView : public views::View {
 public:
  MockView() { SetFocusBehavior(views::View::FocusBehavior::ALWAYS); }

  // views::View:
  MOCK_METHOD(void, OnEvent, (ui::Event*), (override));
};

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

// Adds a simple notification to the `message_center::MessageCenter` with the
// given `id`. If the optional argument `is_system_priority` is true, it will be
// set to the highest priority.
void AddNotification(const std::string& id, bool is_system_priority = false) {
  auto notification =
      SystemNotificationBuilder()
          .SetId(id)
          .SetCatalogName(NotificationCatalogName::kTestCatalogName)
          .SetDelegate(
              base::MakeRefCounted<message_center::NotificationDelegate>())
          .BuildPtr(false);
  if (is_system_priority) {
    notification->SetSystemPriority();
  }
  message_center::MessageCenter::Get()->AddNotification(
      std::move(notification));
}

// Checks the given expected notification counts against the current actual
// values in `message_center::MessageCenter`.
void ExpectNotificationCounts(size_t total_notifications,
                              size_t visible_notifications,
                              size_t popup_notifications) {
  auto* message_center = message_center::MessageCenter::Get();
  EXPECT_EQ(message_center->GetNotifications().size(), total_notifications);
  EXPECT_EQ(message_center->GetVisibleNotifications().size(),
            visible_notifications);
  EXPECT_EQ(message_center->GetPopupNotifications().size(),
            popup_notifications);
}

}  // namespace

// WelcomeTourControllerTest ---------------------------------------------------

// Base class for tests of the `WelcomeTourController`.
class WelcomeTourControllerTest : public UserEducationAshTestBase {
 public:
  WelcomeTourControllerTest() {
    // NOTE: The `WelcomeTourController` exists only when the Welcome Tour
    // feature is enabled. Controller existence is verified in test coverage
    // for the controller's owner.
    scoped_feature_list_.InitAndEnableFeature(features::kWelcomeTour);
  }

 protected:
  // UserEducationAshTestBase:
  void SetUp() override {
    UserEducationAshTestBase::SetUp();

    // Most tests of the `WelcomeTourController` are not concerned with user
    // eligibility, so provide a default implementation of `IsNewUser()` which
    // returns that the given user is "new" on invocation. "New"-ness is
    // required for the user to be eligible for the Welcome Tour.
    ON_CALL(*user_education_delegate(), IsNewUser)
        .WillByDefault(ReturnRefOfCopy(std::make_optional(true)));
  }

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

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

// Verifies that the Welcome Tour is started when the primary user session is
// first activated and then never again, as well as that start/end events are
// propagated to observers appropriately.
TEST_F(WelcomeTourControllerTest, StartsTourAndPropagatesEvents) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");
  const auto secondary_account_id = AccountId::FromUserEmail("secondary@test");

  // Ensure controller exists.
  auto* const welcome_tour_controller = WelcomeTourController::Get();
  ASSERT_TRUE(welcome_tour_controller);

  // Ensure delegate exists and disallow any tutorial registrations/starts.
  auto* const user_education_delegate = this->user_education_delegate();
  ASSERT_TRUE(user_education_delegate);
  EXPECT_CALL(*user_education_delegate, RegisterTutorial).Times(0);
  EXPECT_CALL(*user_education_delegate, StartTutorial).Times(0);

  // Observe the `WelcomeTourController` for start/end events.
  StrictMock<MockWelcomeTourControllerObserver> observer;
  base::ScopedObservation<WelcomeTourController, WelcomeTourControllerObserver>
      observation{&observer};
  observation.Observe(welcome_tour_controller);

  // Add a primary and secondary user session for the first time. This should
  // *not* trigger the Welcome Tour to start.
  auto* const session_controller_client = GetSessionControllerClient();
  session_controller_client->AddUserSession(
      primary_account_id.GetUserEmail(), user_manager::UserType::kRegular,
      /*provide_pref_service=*/true, /*is_new_profile=*/true);
  session_controller_client->AddUserSession(
      secondary_account_id.GetUserEmail(), user_manager::UserType::kRegular,
      /*provide_pref_service=*/true, /*is_new_profile=*/true);

  // Activate the primary user session. This *should* trigger the Welcome Tour
  // to be registered and started as well as notify observers. Note that
  // completed/aborted callbacks are cached for later verification.
  base::OnceClosure completed_callback;
  base::OnceClosure aborted_callback;
  EXPECT_CALL(
      *user_education_delegate,
      RegisterTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
                       TutorialDescriptionEq(
                           welcome_tour_controller->GetTutorialDescription())));
  EXPECT_CALL(
      *user_education_delegate,
      StartTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
                    Eq(welcome_tour_controller->GetInitialElementContext()),
                    /*completed_callback=*/_,
                    /*aborted_callback=*/_))
      .WillOnce(MoveArgs<3, 4>(&completed_callback, &aborted_callback));
  EXPECT_CALL(observer, OnWelcomeTourStarted);
  session_controller_client->SetSessionState(SessionState::ACTIVE);
  Mock::VerifyAndClearExpectations(user_education_delegate);
  Mock::VerifyAndClearExpectations(&observer);

  // The Welcome Tour dialog is expected to be shown at the start of the tour.
  EXPECT_TRUE(WelcomeTourDialog::Get());

  // Disallow any tutorial registrations/starts.
  EXPECT_CALL(*user_education_delegate, RegisterTutorial).Times(0);
  EXPECT_CALL(*user_education_delegate, StartTutorial).Times(0);

  // Switch to the secondary user session and back again. This should *not*
  // trigger the Welcome Tour to start.
  session_controller_client->SwitchActiveUser(secondary_account_id);
  session_controller_client->SwitchActiveUser(primary_account_id);

  // Deactivate and then reactivate the primary user session. This should *not*
  // trigger the Welcome Tour to start.
  session_controller_client->SetSessionState(SessionState::LOCKED);
  session_controller_client->SetSessionState(SessionState::ACTIVE);

  // Verify that the same event is propagated to observers regardless of whether
  // user education services in the browser indicate the tour was completed or
  // aborted. Only if the device is *not* in tablet mode should there be an
  // attempt to launch the Explore app.
  for (base::OnceClosure& ended_callback :
       {std::ref(completed_callback), std::ref(aborted_callback)}) {
    ASSERT_FALSE(ended_callback.is_null());
    EXPECT_CALL(observer, OnWelcomeTourEnded);
    EXPECT_CALL(*user_education_delegate,
                LaunchSystemWebAppAsync(
                    Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
                    Eq(apps::LaunchSource::kFromWelcomeTour),
                    Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
        .Times(display::Screen::GetScreen()->InTabletMode() ? 0u : 1u);
    std::move(ended_callback).Run();
    Mock::VerifyAndClearExpectations(&observer);
    Mock::VerifyAndClearExpectations(user_education_delegate);

    // Aborting the Welcome Tour should close the dialog.
    if (&ended_callback == &aborted_callback) {
      WidgetDestroyedWaiter(WelcomeTourDialog::Get()->GetWidget()).Wait();
      EXPECT_FALSE(WelcomeTourDialog::Get());
    }
  }
}

// Verifies that the Welcome Tour is started when the primary user session is
// first activated and the last active user pref service is not null.
TEST_F(WelcomeTourControllerTest, StartsTourWhenUserPrefServiceIsNotNull) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");

  // Ensure controller exists.
  auto* const welcome_tour_controller = WelcomeTourController::Get();
  ASSERT_TRUE(welcome_tour_controller);

  // Ensure delegate exists and disallow any tutorial registrations/starts.
  auto* const user_education_delegate = this->user_education_delegate();
  ASSERT_TRUE(user_education_delegate);
  EXPECT_CALL(*user_education_delegate, RegisterTutorial).Times(0);
  EXPECT_CALL(*user_education_delegate, StartTutorial).Times(0);

  // Observe the `WelcomeTourController` for start/end events.
  StrictMock<MockWelcomeTourControllerObserver> observer;
  base::ScopedObservation<WelcomeTourController, WelcomeTourControllerObserver>
      observation{&observer};
  observation.Observe(welcome_tour_controller);

  // Add a primary user without pref service for the first time. This should
  // *not* trigger the Welcome Tour to start.
  auto* const session_controller_client = GetSessionControllerClient();
  session_controller_client->AddUserSession(
      primary_account_id.GetUserEmail(), user_manager::UserType::kRegular,
      /*provide_pref_service=*/false, /*is_new_profile=*/true);

  // Activate the primary user session. This should *not* trigger the Welcome
  // Tour to start because the last active user pref service is null.
  session_controller_client->SetSessionState(SessionState::ACTIVE);
  Mock::VerifyAndClearExpectations(user_education_delegate);
  Mock::VerifyAndClearExpectations(&observer);

  // Set the pref service. This *should* trigger the Welcome Tour to be
  // registered and started as well as notify observers.
  EXPECT_CALL(*user_education_delegate, RegisterTutorial).Times(1);
  EXPECT_CALL(*user_education_delegate, StartTutorial).Times(1);
  EXPECT_CALL(observer, OnWelcomeTourStarted);

  session_controller_client->ProvidePrefServiceForUser(primary_account_id);
  Mock::VerifyAndClearExpectations(user_education_delegate);
  Mock::VerifyAndClearExpectations(&observer);
}

// Verifies that the Welcome Tour can be aborted via the dialog.
TEST_F(WelcomeTourControllerTest, AbortsTourAndPropagatesEvents) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");

  // Expect the Welcome Tour to be registered and started when logging in the
  // primary user. Note that the `aborted_callback` is cached.
  base::OnceClosure aborted_callback;
  EXPECT_CALL(*user_education_delegate(),
              RegisterTutorial(
                  Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
                  TutorialDescriptionEq(
                      WelcomeTourController::Get()->GetTutorialDescription())));
  EXPECT_CALL(*user_education_delegate(),
              StartTutorial(Eq(primary_account_id),
                            Eq(TutorialId::kWelcomeTour), _, _, _))
      .WillOnce(MoveArg<4>(&aborted_callback));

  // Start the Welcome Tour by logging in the primary user for the first time.
  SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());

  // Observe the `WelcomeTourController` for end events.
  StrictMock<MockWelcomeTourControllerObserver> observer;
  base::ScopedObservation<WelcomeTourController, WelcomeTourControllerObserver>
      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 clicking the `cancel_button`.
  // Fulfill the request to abort the tour by running the `aborted_callback`.
  EXPECT_CALL(
      *user_education_delegate(),
      AbortTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour)))
      .WillOnce(RunOnceClosure(std::move(aborted_callback)));

  // Expect an attempt to launch the Explore app when the 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())));

  // Click the `cancel_button` and verify the Welcome Tour is ended.
  const views::View* const cancel_button = GetDialogCancelButton();
  ASSERT_TRUE(cancel_button);
  LeftClickOn(cancel_button);
  EXPECT_TRUE(ended_future.Wait());
}

// WelcomeTourControllerV2Test -------------------------------------------------

// Base class for tests of the `WelcomeTourController` which are concerned with
// the behavior of WelcomeTourV2 experiment arms, parameterized by whether the
// Welcome Tour V2 feature is enabled.
class WelcomeTourControllerV2Test
    : public WelcomeTourControllerTest,
      public ::testing::WithParamInterface<std::tuple<
          /*is_welcome_tour_v2_enabled=*/bool,
          /*is_welcome_tour_counterfactually_enabled=*/bool>> {
 public:
  WelcomeTourControllerV2Test() {
    // Only one of those features can be enabled at a time.
    scoped_feature_list_.InitWithFeatureStates(
        {{features::kWelcomeTourV2,
          IsWelcomeTourV2Enabled() && !IsWelcomeTourCounterfactuallyEnabled()},
         {features::kWelcomeTourCounterfactualArm,
          IsWelcomeTourCounterfactuallyEnabled()},
         {features::kWelcomeTourHoldbackArm, false}});
  }

 protected:
  // Returns whether the WelcomeTourV2 feature is enabled given test
  // parameterization.
  bool IsWelcomeTourV2Enabled() const { return std::get<0>(GetParam()); }

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

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

INSTANTIATE_TEST_SUITE_P(
    All,
    WelcomeTourControllerV2Test,
    testing::Combine(
        /*is_welcome_tour_v2_enabled=*/testing::Bool(),
        /*is_welcome_tour_counterfactually_enabled=*/testing::Bool()));

// Verifies that `GetTutorialDescription()` returns expected values.
TEST_P(WelcomeTourControllerV2Test, GetTutorialDescription) {
  auto* welcome_tour_controller = WelcomeTourController::Get();
  ASSERT_TRUE(welcome_tour_controller);

  const std::u16string product_name = ui::GetChromeOSDeviceName();
  const std::u16string total_steps =
      features::IsWelcomeTourV2Enabled() ? kTotalStepsV2 : kTotalStepsV1;
  int current_step = 1;

  using ::testing::Matcher;
  using Description = user_education::TutorialDescription;
  std::vector<Matcher<Description::Step>> expected_steps = {
      ShownStep(ElementSpecifier(kWelcomeTourDialogElementId),
                ContextMode::kAny),
      HiddenStep(ElementSpecifier(kWelcomeTourDialogElementId),
                 ContextMode::kFromPreviousStep),
      BubbleStep(
          ElementSpecifier(kShelfViewElementId), ContextMode::kInitial,
          HelpBubbleId::kWelcomeTourShelf,
          StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_ACCNAME,
                        base::NumberToString16(current_step++), total_steps),
          IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
          StringUTF8Eq(IDS_ASH_WELCOME_TOUR_SHELF_BUBBLE_BODY_TEXT),
          HelpBubbleArrow::kBottomCenter,
          /*has_next_button=*/true),
      EventStep(ElementSpecifier(kShelfViewElementId),
                ContextMode::kFromPreviousStep,
                /*has_name_elements_callback=*/true),
      BubbleStep(
          ElementSpecifier(kUnifiedSystemTrayElementName), ContextMode::kAny,
          HelpBubbleId::kWelcomeTourStatusArea,
          StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_ACCNAME,
                        base::NumberToString16(current_step++), total_steps),
          IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
          StringUTF8Eq(IDS_ASH_WELCOME_TOUR_STATUS_AREA_BUBBLE_BODY_TEXT),
          HelpBubbleArrow::kBottomRight,
          /*has_next_button=*/true),
      EventStep(ElementSpecifier(kUnifiedSystemTrayElementName),
                ContextMode::kFromPreviousStep,
                /*has_name_elements_callback=*/true),
      BubbleStep(
          ElementSpecifier(kHomeButtonElementName), ContextMode::kAny,
          HelpBubbleId::kWelcomeTourHomeButton,
          StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_HOME_BUTTON_BUBBLE_ACCNAME,
                        base::NumberToString16(current_step++), total_steps,
                        product_name),
          IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
          StringFUTF8Eq(
              (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,
                 StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_SEARCH_BOX_BUBBLE_ACCNAME,
                               base::NumberToString16(current_step++),
                               total_steps, product_name),
                 IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
                 StringFUTF8Eq(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)};

  if (features::IsWelcomeTourV2Enabled()) {
    expected_steps.insert(
        expected_steps.end(),
        {// Files app step for V2.
         BubbleStep(
             ElementSpecifier(kFilesAppElementId),
             ContextMode::kFromPreviousStep, HelpBubbleId::kWelcomeTourFilesApp,
             StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_FILES_APP_BUBBLE_ACCNAME,
                           base::NumberToString16(current_step++), total_steps),
             IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
             StringUTF8Eq(IDS_ASH_WELCOME_TOUR_FILES_APP_BUBBLE_BODY_TEXT),
             HelpBubbleArrow::kBottomLeft,
             /*has_next_button=*/true),
         EventStep(ElementSpecifier(kFilesAppElementId),
                   ContextMode::kFromPreviousStep,
                   /*has_name_elements_callback=*/false)});
  }

  expected_steps.insert(
      expected_steps.end(),
      {BubbleStep(
           ElementSpecifier(kSettingsAppElementId),
           ContextMode::kFromPreviousStep,
           HelpBubbleId::kWelcomeTourSettingsApp,
           StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_SETTINGS_APP_BUBBLE_ACCNAME,
                         base::NumberToString16(current_step++), total_steps,
                         product_name),
           IDS_ASH_WELCOME_TOUR_OVERRIDDEN_BUBBLE_BODY_TEXT,
           StringFUTF8Eq(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_OVERRIDDEN_BUBBLE_BODY_TEXT,
           StringFUTF8Eq(IDS_ASH_WELCOME_TOUR_EXPLORE_APP_BUBBLE_BODY_TEXT,
                         product_name),
           HelpBubbleArrow::kBottomLeft,
           /*has_next_button=*/false)});

  EXPECT_THAT(welcome_tour_controller->GetTutorialDescription(),
              AllOf(Field(&TutorialDescription::complete_button_text_id,
                          Eq(IDS_ASH_WELCOME_TOUR_COMPLETE_BUTTON_TEXT)),
                    Field(&TutorialDescription::steps,
                          ElementsAreArray(expected_steps))));
}

// 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());

  // Observe the `WelcomeTourController` for end events.
  StrictMock<MockWelcomeTourControllerObserver> observer;
  base::ScopedObservation<WelcomeTourController, WelcomeTourControllerObserver>
      observation{&observer};
  observation.Observe(WelcomeTourController::Get());

  // 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)))
      .Times(expect_abort ? 1 : 0);

  // 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())))
      .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()));
}

// 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,
      /*provide_pref_service=*/true, /*is_new_profile=*/true);
  session->SwitchActiveUser(primary_account_id);

  // Enable the spoken feedback after the pref service is ready and before the
  // session becomes active.
  auto* const a11y_controller = Shell::Get()->accessibility_controller();
  a11y_controller->SetSpokenFeedbackEnabled(true, A11Y_NOTIFICATION_NONE);
  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 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())))
      .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()));
}

// WelcomeTourControllerHoldbackTest -------------------------------------------

// Base class for tests of the `WelcomeTourController` which are concerned
// with the behavior of holdback experiment arms, parameterized by whether the
// Welcome Tour is held back.
class WelcomeTourControllerHoldbackTest
    : public WelcomeTourControllerTest,
      public ::testing::WithParamInterface<
          /*is_holdback=*/std::optional<bool>> {
 public:
  WelcomeTourControllerHoldbackTest() {
    // Only one of those features can be enabled at a time.
    if (const auto& is_holdback = IsHoldback()) {
      scoped_feature_list_.InitWithFeatureStates(
          {{features::kWelcomeTourHoldbackArm, is_holdback.value()},
           {features::kWelcomeTourV2, false},
           {features::kWelcomeTourCounterfactualArm, false}});
    }
  }

  // Returns whether the Welcome Tour is enabled as the holdback experiment
  // arm given test parameterization.
  const std::optional<bool>& IsHoldback() const { return GetParam(); }

 private:
  // Used to conditionally enable the Welcome Tour as the holdback experiment
  // arm given test parameterization.
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         WelcomeTourControllerHoldbackTest,
                         /*is_holdback=*/
                         ::testing::Values(std::make_optional(true),
                                           std::make_optional(false),
                                           std::nullopt));

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

// Verifies that the Welcome Tour is prevented from running if enabled as the
// holdback experiment arm and that an attempt is made to launch the Explore app
// when that occurs.
TEST_P(WelcomeTourControllerHoldbackTest, PreventsWelcomeTourForHoldbackArms) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");

  // Set expectations for whether the Welcome Tour will run.
  EXPECT_CALL(
      *user_education_delegate(),
      RegisterTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour), _))
      .Times(IsHoldback().value_or(false) ? 0u : 1u);
  EXPECT_CALL(*user_education_delegate(),
              StartTutorial(Eq(primary_account_id),
                            Eq(TutorialId::kWelcomeTour), _, _, _))
      .Times(IsHoldback().value_or(false) ? 0u : 1u);

  // Set expectations for whether the Explore app will launch.
  EXPECT_CALL(*user_education_delegate(),
              LaunchSystemWebAppAsync(
                  Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
                  Eq(apps::LaunchSource::kFromWelcomeTour),
                  Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
      .Times(IsHoldback().value_or(false) ? 1u : 0u);

  // Login the primary user for the first time and verify expectations.
  SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());
  Mock::VerifyAndClearExpectations(user_education_delegate());
}

// WelcomeTourControllerUserEligibilityTest ------------------------------------

// Base class for tests of the `WelcomeTourController` which are concerned with
// user eligibility, parameterized by:
// (a) whether to force user eligibility via feature flag
// (b) whether the user should be considered "new" cross-device
// (c) whether the user should be considered "new" locally
// (d) whether the user is managed
// (e) the user type.
class WelcomeTourControllerUserEligibilityTest
    : public WelcomeTourControllerTest,
      public ::testing::WithParamInterface<std::tuple<
          /*force_user_eligibility=*/bool,
          /*is_new_user_cross_device=*/std::optional<bool>,
          /*is_new_user_locally=*/bool,
          /*is_managed_user=*/bool,
          user_manager::UserType>> {
 public:
  WelcomeTourControllerUserEligibilityTest() {
    // Conditionally force user eligibility based on test parameterization.
    scoped_feature_list_.InitWithFeatureState(
        features::kWelcomeTourForceUserEligibility, ForceUserEligibility());
  }

  // Returns the account ID of the primary user for whom to test eligibility.
  const AccountId& primary_account_id() const { return primary_account_id_; }

  // Returns whether user eligibility is forced based on test parameterization.
  bool ForceUserEligibility() const { return std::get<0>(GetParam()); }

  // Returns the user type based on test parameterization.
  user_manager::UserType GetUserType() const { return std::get<4>(GetParam()); }

  // Returns whether the user is managed based on test parameterization.
  bool IsManagedUser() const { return std::get<3>(GetParam()); }

  // Returns whether the user should be considered "new" cross-device based on
  // test parameterization.
  const std::optional<bool>& IsNewUserCrossDevice() const {
    return std::get<1>(GetParam());
  }

  // Returns whether the user should be considered "new" locally based on test
  // parameterization.
  bool IsNewUserLocally() const { return std::get<2>(GetParam()); }

 private:
  // WelcomeTourControllerTest:
  void SetUp() override {
    WelcomeTourControllerTest::SetUp();

    // Provide an implementation of `IsNewUser()` which returns whether a given
    // user should be considered "new" cross-device based on test
    // parameterization.
    ON_CALL(*user_education_delegate(), IsNewUser)
        .WillByDefault(ReturnRefOfCopy(IsNewUserCrossDevice()));

    // Add a user based on test parameterization.
    TestSessionControllerClient* const session = GetSessionControllerClient();
    session->AddUserSession(primary_account_id_.GetUserEmail(), GetUserType(),
                            /*provide_pref_service=*/true,
                            /*is_new_profile=*/IsNewUserLocally(),
                            /*given_name=*/std::string(), IsManagedUser());
    session->SwitchActiveUser(primary_account_id_);
  }

  // Used to conditionally force user eligibility based on test
  // parameterization.
  base::test::ScopedFeatureList scoped_feature_list_;

  // The account ID of the primary user for whom to test eligibility.
  const AccountId primary_account_id_ =
      AccountId::FromUserEmail("primary@test");
};

INSTANTIATE_TEST_SUITE_P(
    All,
    WelcomeTourControllerUserEligibilityTest,
    ::testing::Combine(
        /*force_user_eligibility=*/::testing::Bool(),
        /*is_new_user_cross_device=*/
        ::testing::Values(std::make_optional(true),
                          std::make_optional(false),
                          std::nullopt),
        /*is_new_user_locally=*/::testing::Bool(),
        /*is_managed_user=*/::testing::Bool(),
        ::testing::Values(user_manager::UserType::kChild,
                          user_manager::UserType::kGuest,
                          user_manager::UserType::kKioskApp,
                          user_manager::UserType::kPublicAccount,
                          user_manager::UserType::kRegular,
                          user_manager::UserType::kWebKioskApp)));

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

// Verifies that user eligibility for the Welcome Tour is enforced as expected.
TEST_P(WelcomeTourControllerUserEligibilityTest, EnforcesUserEligibility) {
  // A user is eligible for the Welcome Tour if and only if:
  // (a) user eligibility is being explicitly forced, or
  // (b) the user satisfies the following conditions:
  //     (1) known to be "new" cross-device on session activation, and
  //     (2) known to be "new" locally, and
  //     (3) not a managed user, and
  //     (4) a regular user.
  const bool is_user_eligibility_expected =
      ForceUserEligibility() ||
      (IsNewUserCrossDevice().value_or(false) && IsNewUserLocally() &&
       !IsManagedUser() && GetUserType() == user_manager::UserType::kRegular);

  // Set expectations for whether the Welcome Tour will run.
  EXPECT_CALL(*user_education_delegate(),
              RegisterTutorial(Eq(primary_account_id()),
                               Eq(TutorialId::kWelcomeTour), _))
      .Times(is_user_eligibility_expected ? 1u : 0u);
  EXPECT_CALL(*user_education_delegate(),
              StartTutorial(Eq(primary_account_id()),
                            Eq(TutorialId::kWelcomeTour), _, _, _))
      .Times(is_user_eligibility_expected ? 1u : 0u);

  // If the Welcome Tour is run, we delay attempts to launch the Explore app
  // until the tour is completed or aborted. If the Welcome Tour is not run, the
  // user is not new so there should similarly be no attempt to launch Explore.
  EXPECT_CALL(*user_education_delegate(),
              LaunchSystemWebAppAsync(
                  Eq(primary_account_id()), Eq(ash::SystemWebAppType::HELP),
                  Eq(apps::LaunchSource::kFromWelcomeTour),
                  Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
      .Times(0);

  base::HistogramTester histogram_tester;

  // Activate the user session and verify expectations.
  GetSessionControllerClient()->SetSessionState(SessionState::ACTIVE);
  Mock::VerifyAndClearExpectations(user_education_delegate());

  // Verify histograms.
  // NOTE: Order is important. For users not going through OOBE, it is expected
  // that cross-device newness be unavailable. To ensure that the most specific
  // prevention reason is emitted to histograms, cross-device newness checks
  // should be last.
  std::vector<base::Bucket> buckets;
  if (!ForceUserEligibility()) {
    if (GetUserType() != user_manager::UserType::kRegular) {
      buckets.emplace_back(PreventedReason::kUserTypeNotRegular, 1);
    } else if (IsManagedUser()) {
      buckets.emplace_back(PreventedReason::kManagedAccount, 1);
    } else if (!IsNewUserLocally()) {
      buckets.emplace_back(PreventedReason::kUserNotNewLocally, 1);
    } else if (!IsNewUserCrossDevice().has_value()) {
      buckets.emplace_back(PreventedReason::kUserNewnessNotAvailable, 1);
    } else if (!IsNewUserCrossDevice().value()) {
      buckets.emplace_back(PreventedReason::kUserNotNewCrossDevice, 1);
    }
  }
  EXPECT_THAT(
      histogram_tester.GetAllSamples("Ash.WelcomeTour.Prevented.Reason"),
      base::BucketsAreArray(buckets));
}

// WelcomeTourControllerRunTest ------------------------------------------------

// Base class for tests of the `WelcomeTourController` that run the Welcome
// Tour in order to assert expectations before, during, and/or after run time.
class WelcomeTourControllerRunTest : public WelcomeTourControllerTest {
 public:
  WelcomeTourControllerRunTest() = default;

  // Runs the Welcome Tour, invoking the specified `in_progress_callback` just
  // after the Welcome Tour has started. Note that this method will not return
  // until the Welcome Tour has ended.
  void Run(base::OnceClosure in_progress_callback) {
    // Ensure `controller` exists.
    auto* const controller = WelcomeTourController::Get();
    ASSERT_TRUE(controller);

    // Ensure `delegate` exists.
    auto* const delegate = user_education_delegate();
    ASSERT_TRUE(delegate);

    // Observe the `controller` for Welcome Tour start/end events.
    StrictMock<MockWelcomeTourControllerObserver> observer;
    base::ScopedObservation<WelcomeTourController,
                            WelcomeTourControllerObserver>
        observation{&observer};
    observation.Observe(controller);

    // When the Welcome Tour starts/ends, signal the appropriate future.
    base::test::TestFuture<void> started_future;
    base::test::TestFuture<void> ended_future;
    EXPECT_CALL(observer, OnWelcomeTourStarted)
        .WillOnce(RunOnceClosure(started_future.GetCallback()));
    EXPECT_CALL(observer, OnWelcomeTourEnded)
        .WillOnce(RunOnceClosure(ended_future.GetCallback()));

    const auto primary_account_id = AccountId::FromUserEmail("primary@test");

    // When the Welcome Tour tutorial is registered and started, cache the
    // callback to invoke to complete the tutorial.
    base::OnceClosure completed_callback;
    EXPECT_CALL(
        *delegate,
        RegisterTutorial(
            Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
            TutorialDescriptionEq(controller->GetTutorialDescription())));
    EXPECT_CALL(*delegate, StartTutorial(Eq(primary_account_id),
                                         Eq(TutorialId::kWelcomeTour), _, _, _))
        .WillOnce(MoveArg<3>(&completed_callback));

    // Simulate login of the primary user for the first time. Note that this
    // should trigger the Welcome Tour to start automatically.
    SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());
    EXPECT_TRUE(started_future.Wait());

    // Invoke the `in_progress_callback` so that tests can assert expectations
    // while the Welcome Tour is in progress.
    std::move(in_progress_callback).Run();

    // When the tour is completed, expect an attempt to launch the Explore app.
    EXPECT_CALL(
        *user_education_delegate(),
        LaunchSystemWebAppAsync(
            Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
            Eq(apps::LaunchSource::kFromWelcomeTour),
            Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())));

    // Click `accept_button` to close the Welcome Tour dialog.
    const views::View* const accept_button = GetDialogAcceptButton();
    ASSERT_TRUE(accept_button);
    LeftClickOn(accept_button);

    // Complete the tutorial by invoking the cached callback.
    std::move(completed_callback).Run();
    EXPECT_TRUE(ended_future.Wait());
    Mock::VerifyAndClearExpectations(user_education_delegate());
  }
};

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

TEST_F(WelcomeTourControllerRunTest, BlockInteractionsWithIrrelevantWindow) {
  // Create a random widget to interact with.
  std::unique_ptr<views::Widget> widget =
      TestWidgetBuilder()
          .SetBounds(gfx::Rect(100, 100))
          .SetParent(Shell::GetPrimaryRootWindow()->GetChildById(
              kShellWindowId_LockScreenContainer))
          .SetWidgetType(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS)
          .BuildOwnsNativeWidget();
  MockView* const contents_view =
      widget->SetContentsView(std::make_unique<MockView>());
  widget->GetFocusManager()->SetFocusedView(contents_view);

  // `contents_view` should be interactive before the Welcome Tour.
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMouseEntered))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMousePressed))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMouseExited))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kKeyPressed))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kKeyReleased))));
  LeftClickOn(contents_view);
  PressAndReleaseKey(ui::VKEY_A);

  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        // During the Welcome Tour, `contents_view` should NOT be interactive.
        EXPECT_CALL(*contents_view, OnEvent).Times(0);
        LeftClickOn(contents_view);
        PressAndReleaseKey(ui::VKEY_A);
      })));

  // Perform the same set of actions after the Welcome Tour. `contents_view`
  // should be interactive.
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMouseEntered))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMousePressed))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kMouseExited))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kKeyPressed))));
  EXPECT_CALL(
      *contents_view,
      OnEvent(Property(&ui::Event::type, Eq(ui::EventType::kKeyReleased))));
  LeftClickOn(contents_view);
  PressAndReleaseKey(ui::VKEY_A);
}

// Verifies that notifications are blocked during and only during the Welcome
// Tour.
TEST_F(WelcomeTourControllerRunTest, NotificationBlocking) {
  constexpr char kSystemPriorityId[] = "system";
  auto* message_center = message_center::MessageCenter::Get();

  // Case: Before Welcome Tour.
  // Most notifications should be muted, with few exceptions, because no user is
  // logged in.
  {
    SCOPED_TRACE("Initial state");
    ExpectNotificationCounts(/*total_notifications=*/0u,
                             /*visible_notifications=*/0u,
                             /*popup_notifications=*/0u);
  }

  {
    SCOPED_TRACE("Simple notification before login");
    AddNotification("test1");
    ExpectNotificationCounts(
        /*total_notifications=*/1u,
        /*visible_notifications=*/0u,
        /*popup_notifications=*/0u);
  }

  {
    SCOPED_TRACE("Battery notification before login");

    // Use the id from `BatteryNotification` to confirm that those notifications
    // are not muted, as battery notifications show even during OOBE. Note that
    // it does not have system priority; the OOBE exception is based on its id.
    AddNotification(BatteryNotification::kNotificationId);
    ExpectNotificationCounts(
        /*total_notifications=*/2u,
        /*visible_notifications=*/1u,
        /*popup_notifications=*/1u);
  }

  // Case: During Welcome Tour.
  // Notification blocking should hide notifications, including hiding any
  // existing popups.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        {
          SCOPED_TRACE("Beginning of Welcome Tour");
          ExpectNotificationCounts(
              /*total_notifications=*/2u,
              /*visible_notifications=*/0u,
              /*popup_notifications=*/0u);
        }

        {
          SCOPED_TRACE("Simple notification during Welcome Tour");
          AddNotification("test2");
          ExpectNotificationCounts(
              /*total_notifications=*/3u,
              /*visible_notifications=*/0u,
              /*popup_notifications=*/0u);
        }

        {
          SCOPED_TRACE("System priority notification during Welcome Tour");

          // System priority notifications should show a popup after the tour is
          // over.
          AddNotification(kSystemPriorityId, /*is_system_priority=*/true);
          ExpectNotificationCounts(
              /*total_notifications=*/4u,
              /*visible_notifications=*/0u,
              /*popup_notifications=*/0u);
        }
      })));

  // Case: After Welcome Tour.
  // All notifications should now be shown in the list. Notifications that
  // happened before or during the tour should not have popups, but new
  // notifications should.
  {
    SCOPED_TRACE("Just after Welcome Tour");
    ExpectNotificationCounts(
        /*total_notifications=*/4u,
        /*visible_notifications=*/4u,
        /*popup_notifications=*/1u);
  }

  // Confirm that the one popup showing is the one with system priority.
  EXPECT_THAT(message_center->GetPopupNotifications(),
              ElementsAre(Property(&message_center::Notification::id,
                                   Eq(kSystemPriorityId))));

  {
    SCOPED_TRACE("Notification after Welcome Tour");
    AddNotification("test3");
    ExpectNotificationCounts(
        /*total_notifications=*/5u,
        /*visible_notifications=*/5u,
        /*popup_notifications=*/2u);
  }
}

// Verifies that nudges are suppressed while the Welcome Tour is in progress.
TEST_F(WelcomeTourControllerRunTest, NudgePause) {
  static constexpr char kNudgeId[] = "nudge_id";

  // Verify nudge manager exists.
  auto* const nudge_manager = AnchoredNudgeManager::Get();
  ASSERT_TRUE(nudge_manager);

  // Cache helper to check if a nudge matching `kNudgeId` is showing.
  auto is_showing_nudge = [&]() {
    return nudge_manager->IsNudgeShown(kNudgeId);
  };

  // Cache helper to show a toast matching `kNudgeId`.
  auto show_nudge = [&]() {
    AnchoredNudgeData nudge(kNudgeId, NudgeCatalogName::kMaxValue, u"text");
    nudge_manager->Show(nudge);
  };

  // Case: Before Welcome Tour.
  EXPECT_FALSE(is_showing_nudge());
  show_nudge();
  EXPECT_TRUE(is_showing_nudge());

  // Case: During Welcome Tour.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        EXPECT_FALSE(is_showing_nudge());
        show_nudge();
        EXPECT_FALSE(is_showing_nudge());
      })));

  // Case: After Welcome Tour.
  EXPECT_FALSE(is_showing_nudge());
  show_nudge();
  EXPECT_TRUE(is_showing_nudge());
}

// Verifies that scrims are added to all root windows only while the Welcome
// Tour is in progress.
TEST_F(WelcomeTourControllerRunTest, Scrim) {
  // Case: Before Welcome Tour.
  ExpectScrimsOnAllRootWindows(false);

  // Case: During Welcome Tour.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting(
          [&]() { ExpectScrimsOnAllRootWindows(true); })));

  // Case: After Welcome Tour.
  ExpectScrimsOnAllRootWindows(false);
}

// Verifies that toasts are suppressed while the Welcome Tour is in progress.
TEST_F(WelcomeTourControllerRunTest, ToastPause) {
  static constexpr char kToastId[] = "toast_id";

  // Verify toast manager exists.
  auto* const toast_manager = ToastManager::Get();
  ASSERT_TRUE(toast_manager);

  // Cache helper to check if a toast matching `kToastId` is showing.
  auto is_showing_toast = [&]() {
    return toast_manager->IsToastShown(kToastId);
  };

  // Cache helper to show a toast matching `kToastId`.
  auto show_toast = [&]() {
    toast_manager->Show(ToastData(kToastId, ToastCatalogName::kMaxValue,
                                  u"text", ToastData::kInfiniteDuration,
                                  /*visible_on_lock_screen=*/true));
  };

  // Case: Before Welcome Tour.
  EXPECT_FALSE(is_showing_toast());
  show_toast();
  EXPECT_TRUE(is_showing_toast());

  // Case: During Welcome Tour.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        EXPECT_FALSE(is_showing_toast());
        show_toast();
        EXPECT_FALSE(is_showing_toast());
      })));

  // Case: After Welcome Tour.
  EXPECT_FALSE(is_showing_toast());
  show_toast();
  EXPECT_TRUE(is_showing_toast());
}

// Verifies that windows are minimized iff the Welcome Tour is in progress.
TEST_F(WelcomeTourControllerRunTest, WindowMinimizer) {
  auto window_1 = CreateAppWindow();

  // Case: Before Welcome Tour.
  EXPECT_THAT(window_1, Minimized(Eq(false)));

  // Case: During Welcome Tour.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        EXPECT_TRUE(WaitUntilMinimized(window_1.get()));
        auto window_2 = CreateAppWindow();
        EXPECT_TRUE(WaitUntilMinimized(window_2.get()));
      })));

  // Case: After Welcome Tour.
  EXPECT_THAT(window_1, Minimized(Eq(true)));
  auto window_3 = CreateAppWindow();
  EXPECT_THAT(window_3, Minimized(Eq(false)));
}

// Verifies accelerator actions before/during/after the Welcome Tour.
class WelcomeTourAcceleratorHandlerRunTest
    : public WelcomeTourControllerRunTest {
 public:
  // WelcomeTourControllerRunTest:
  void SetUp() override {
    WelcomeTourControllerRunTest::SetUp();

    // Create a mock pre-target event handler that always consumes key events.
    mock_pretarget_event_handler_ =
        std::make_unique<MockPretargetEventHandler>();
    ON_CALL(*mock_pretarget_event_handler_, OnKeyEvent)
        .WillByDefault(
            [](ui::KeyEvent* key_event) { key_event->StopPropagation(); });
  }

  void TearDown() override {
    mock_pretarget_event_handler_.reset();
    WelcomeTourControllerRunTest::TearDown();
  }

  // Performs the specified accelerator action. Then checks the key events
  // received as expected.
  void PerformActionAndCheckKeyEvents(AcceleratorAction action, bool received) {
    // Get the accelerators corresponding to `action`.
    const std::vector<AcceleratorDetails>& accelerators_details =
        Shell::Get()->accelerator_lookup()->GetAcceleratorsForAction(action);
    ASSERT_FALSE(accelerators_details.empty());

    for (const AcceleratorDetails& accelerator_details : accelerators_details) {
      // If `received` is true, then `accelerator` should be received;
      // otherwise, `accelerator` should NOT be received.
      const ui::Accelerator accelerator = accelerator_details.accelerator;
      EXPECT_CALL(
          *mock_pretarget_event_handler_,
          OnKeyEvent(AllOf(
              Property(&ui::KeyEvent::type,
                       Eq(accelerator.key_state() ==
                                  ui::Accelerator::KeyState::PRESSED
                              ? ui::EventType::kKeyPressed
                              : ui::EventType::kKeyReleased)),
              Property(&ui::KeyEvent::key_code, Eq(accelerator.key_code())))))
          .Times(received ? 1u : 0u);

      // The key event that does NOT trigger `action` should be received.
      EXPECT_CALL(
          *mock_pretarget_event_handler_,
          OnKeyEvent(AllOf(
              Property(&ui::KeyEvent::type,
                       Eq(accelerator.key_state() ==
                                  ui::Accelerator::KeyState::PRESSED
                              ? ui::EventType::kKeyReleased
                              : ui::EventType::kKeyPressed)),
              Property(&ui::KeyEvent::key_code, Eq(accelerator.key_code())))));

      // Press and release `accelerator`.
      PressAndReleaseKey(accelerator.key_code(), accelerator.modifiers());
      Mock::VerifyAndClearExpectations(mock_pretarget_event_handler_.get());
    }
  }

  void VerifyActionsInAllowedList() {
    for (auto allowed_action : WelcomeTourAcceleratorHandler::kAllowedActions) {
      PerformActionAndCheckKeyEvents(allowed_action.action,
                                     /*received=*/true);
    }
  }

  std::unique_ptr<MockPretargetEventHandler> mock_pretarget_event_handler_;
};

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

// Verifies that the key events that trigger the allowed accelerator actions are
// received during the Welcome Tour.
TEST_F(WelcomeTourAcceleratorHandlerRunTest, AllowActionsInAllowedList) {
  // Verify that before the Welcome Tour, the key events for the actions in the
  // allowed list are received by the mock event handler.
  VerifyActionsInAllowedList();

  // Verify that during the Welcome Tour, the key events for these actions are
  // received by the mock event handler.
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting(
          [&]() { VerifyActionsInAllowedList(); })));

  // Verify that after the Welcome Tour, the key events for these actions are
  // received by the mock event handler.
  VerifyActionsInAllowedList();
}

// Verifies that the accelerator actions NOT in the allowed list should be
// blocked during the Welcome Tour.
TEST_F(WelcomeTourAcceleratorHandlerRunTest, BlockActionsNotInAllowedList) {
  // Verify that before the Welcome Tour, the key events for the actions that
  // are NOT in the allowed list are received by the mock event handler.
  PerformActionAndCheckKeyEvents(AcceleratorAction::kTakePartialScreenshot,
                                 /*received=*/true);

  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        // During the Welcome Tour, the key events that trigger these actions
        // should NOT be received by the mock event handler.
        PerformActionAndCheckKeyEvents(
            AcceleratorAction::kTakePartialScreenshot,
            /*received=*/false);
      })));

  // Verify that after the Welcome Tour, these key events are received by
  // the mock event handler.
  PerformActionAndCheckKeyEvents(AcceleratorAction::kTakePartialScreenshot,
                                 /*received=*/true);
}

// Verifies the accelerator actions that abort the Welcome Tour when performed.
TEST_F(WelcomeTourAcceleratorHandlerRunTest, CheckActionsThatAbortTour) {
  ASSERT_NO_FATAL_FAILURE(
      Run(/*in_progress_callback=*/base::BindLambdaForTesting([&]() {
        for (const auto& allowed_action :
             WelcomeTourAcceleratorHandler::kAllowedActions) {
          if (!allowed_action.aborts_tour) {
            // Skip `allowed_action` if it does not need to abort the tour.
            continue;
          }

          base::test::TestFuture<void> aborted_future;
          EXPECT_CALL(*user_education_delegate(), AbortTutorial)
              .WillOnce(RunOnceClosure(aborted_future.GetCallback()));

          // During the Welcome Tour, the key events for `action` should be
          // received by the mock event handler.
          PerformActionAndCheckKeyEvents(allowed_action.action,
                                         /*received=*/true);

          // The delegate API that aborts the Welcome Tour should be called.
          EXPECT_TRUE(aborted_future.Wait());
          Mock::VerifyAndClearExpectations(user_education_delegate());
        }
      })));
}

// WelcomeTourControllerTabletTest ---------------------------------------------

// Base class for tests of the `WelcomeTourController` that verify the tour does
// not start/will abort in tablet mode.
class WelcomeTourControllerTabletTest : public WelcomeTourControllerTest {
 public:
  WelcomeTourController* controller() { return WelcomeTourController::Get(); }
  StrictMock<MockWelcomeTourControllerObserver>* observer() {
    return &observer_;
  }

  // WelcomeTourControllerTest:
  void SetUp() override {
    WelcomeTourControllerTest::SetUp();

    // Observe the `WelcomeTourController` for start/end events.
    observation_.Observe(controller());
  }

  void TearDown() override {
    observation_.Reset();

    WelcomeTourControllerTest::TearDown();
  }

 private:
  StrictMock<MockWelcomeTourControllerObserver> observer_;
  base::ScopedObservation<WelcomeTourController, WelcomeTourControllerObserver>
      observation_{&observer_};
};

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

// Verifies that the Welcome Tour will not start when in tablet mode.
TEST_F(WelcomeTourControllerTabletTest, DoesNotStart) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");

  // Force tablet mode on.
  TabletMode::Get()->SetEnabledForTest(true);

  // Activate the primary user session for the first time. Since tablet mode is
  // enabled, the Welcome Tour should not be registered or started, the dialog
  // should not show, there should be no attempt to launch the Explore app, and
  // no start or end calls should be made.
  EXPECT_CALL(*user_education_delegate(), RegisterTutorial).Times(0);
  EXPECT_CALL(*user_education_delegate(), StartTutorial).Times(0);
  EXPECT_CALL(*user_education_delegate(),
              LaunchSystemWebAppAsync(
                  Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
                  Eq(apps::LaunchSource::kFromWelcomeTour),
                  Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
      .Times(0);
  SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());
  EXPECT_FALSE(WelcomeTourDialog::Get());
  Mock::VerifyAndClearExpectations(user_education_delegate());
  Mock::VerifyAndClearExpectations(observer());
}

// Verifies that the tour will abort if we enter tablet mode.
TEST_F(WelcomeTourControllerTabletTest, TriggersAbort) {
  const auto primary_account_id = AccountId::FromUserEmail("primary@test");

  // Activate the user session for the first time to trigger the Welcome Tour to
  // be registered and started, as well as notify observers. Note that the
  // aborted callback is cached for later verification.
  base::OnceClosure aborted_callback;
  EXPECT_CALL(*user_education_delegate(),
              RegisterTutorial(
                  Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
                  TutorialDescriptionEq(
                      WelcomeTourController::Get()->GetTutorialDescription())));
  EXPECT_CALL(
      *user_education_delegate(),
      StartTutorial(Eq(primary_account_id), Eq(TutorialId::kWelcomeTour),
                    /*element_context=*/_,
                    /*completed_callback=*/_,
                    /*aborted_callback=*/_))
      .WillOnce(MoveArgs<4>(&aborted_callback));
  EXPECT_CALL(*observer(), OnWelcomeTourStarted);
  SimulateNewUserFirstLogin(primary_account_id.GetUserEmail());
  Mock::VerifyAndClearExpectations(user_education_delegate());
  Mock::VerifyAndClearExpectations(observer());
  ASSERT_FALSE(aborted_callback.is_null());

  // Force tablet mode on, which should cause the tutorial to abort. Because
  // the device is in tablet mode, no attempt should be made to launch the
  // Explore app.
  EXPECT_CALL(*user_education_delegate(), AbortTutorial)
      .WillOnce(RunOnceClosure(std::move(aborted_callback)));
  EXPECT_CALL(*user_education_delegate(),
              LaunchSystemWebAppAsync(
                  Eq(primary_account_id), Eq(ash::SystemWebAppType::HELP),
                  Eq(apps::LaunchSource::kFromWelcomeTour),
                  Eq(display::Screen::GetScreen()->GetPrimaryDisplay().id())))
      .Times(0);
  EXPECT_CALL(*observer(), OnWelcomeTourEnded);
  TabletMode::Get()->SetEnabledForTest(true);
  Mock::VerifyAndClearExpectations(observer());
  Mock::VerifyAndClearExpectations(user_education_delegate());

  // Wait for the dialog widget to be destroyed.
  ASSERT_THAT(WelcomeTourDialog::Get(),
              Property(&WelcomeTourDialog::GetWidget, NotNull()));
  WidgetDestroyedWaiter(WelcomeTourDialog::Get()->GetWidget()).Wait();
  EXPECT_FALSE(WelcomeTourDialog::Get());
}

}  // namespace ash