| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/user_education/common/tutorial.h" |
| |
| #include <string> |
| |
| #include "base/test/bind.h" |
| #include "base/test/mock_callback.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/user_education/common/help_bubble_factory_registry.h" |
| #include "components/user_education/common/help_bubble_params.h" |
| #include "components/user_education/common/tutorial_description.h" |
| #include "components/user_education/common/tutorial_identifier.h" |
| #include "components/user_education/common/tutorial_registry.h" |
| #include "components/user_education/common/tutorial_service.h" |
| #include "components/user_education/test/test_help_bubble.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/interaction/element_test_util.h" |
| #include "ui/base/interaction/element_tracker.h" |
| #include "ui/base/interaction/expect_call_in_scope.h" |
| #include "ui/base/interaction/interaction_sequence.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace user_education { |
| |
| namespace { |
| |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestIdentifier1); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestIdentifier2); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestIdentifier3); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kCustomEventType1); |
| |
| const char kTestElementName1[] = "ELEMENT_NAME_1"; |
| |
| const ui::ElementContext kTestContext1(1); |
| |
| class TestTutorialService : public TutorialService { |
| public: |
| TestTutorialService(TutorialRegistry* tutorial_registry, |
| HelpBubbleFactoryRegistry* help_bubble_factory_registry) |
| : TutorialService(tutorial_registry, help_bubble_factory_registry) {} |
| ~TestTutorialService() override = default; |
| |
| std::u16string GetBodyIconAltText(bool is_last_step) const override { |
| return std::u16string(); |
| } |
| }; |
| |
| std::unique_ptr<HelpBubbleFactoryRegistry> |
| CreateTestTutorialBubbleFactoryRegistry() { |
| auto bubble_factory_registry = std::make_unique<HelpBubbleFactoryRegistry>(); |
| bubble_factory_registry->MaybeRegister<test::TestHelpBubbleFactory>(); |
| return bubble_factory_registry; |
| } |
| |
| void ClickDismissButton(HelpBubble* bubble) { |
| auto* const help_bubble = static_cast<test::TestHelpBubble*>(bubble); |
| help_bubble->SimulateDismiss(); |
| } |
| |
| void ClickCloseButton(HelpBubble* bubble) { |
| auto* const help_bubble = static_cast<test::TestHelpBubble*>(bubble); |
| int button_index = help_bubble->GetIndexOfButtonWithText( |
| l10n_util::GetStringUTF16(IDS_TUTORIAL_CLOSE_TUTORIAL)); |
| EXPECT_TRUE(button_index != test::TestHelpBubble::kNoButtonWithTextIndex); |
| help_bubble->SimulateButtonPress(button_index); |
| } |
| |
| void ClickRestartButton(HelpBubble* bubble) { |
| auto* const help_bubble = static_cast<test::TestHelpBubble*>(bubble); |
| int button_index = help_bubble->GetIndexOfButtonWithText( |
| l10n_util::GetStringUTF16(IDS_TUTORIAL_RESTART_TUTORIAL)); |
| |
| EXPECT_TRUE(button_index != test::TestHelpBubble::kNoButtonWithTextIndex); |
| help_bubble->SimulateButtonPress(button_index); |
| } |
| |
| const TutorialIdentifier kTestTutorial1{"kTestTutorial1"}; |
| } // namespace |
| |
| class TutorialTest : public testing::Test {}; |
| |
| TEST_F(TutorialTest, TutorialBuilder) { |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| Tutorial::Builder builder; |
| |
| // build a step with an ElementID |
| std::unique_ptr<ui::InteractionSequence::Step> step1 = |
| Tutorial::StepBuilder() |
| .SetAnchorElementID(kTestIdentifier1) |
| .Build(&service); |
| |
| // build a step that names an element |
| std::unique_ptr<ui::InteractionSequence::Step> step2 = |
| Tutorial::StepBuilder() |
| .SetAnchorElementID(kTestIdentifier1) |
| .SetNameElementsCallback( |
| base::BindRepeating([](ui::InteractionSequence* sequence, |
| ui::TrackedElement* element) { |
| sequence->NameElement(element, "TEST ELEMENT"); |
| return true; |
| })) |
| .Build(&service); |
| |
| // build a step with a named element |
| std::unique_ptr<ui::InteractionSequence::Step> step3 = |
| Tutorial::StepBuilder() |
| .SetAnchorElementName(std::string(kTestElementName1)) |
| .Build(&service); |
| |
| // transition event |
| std::unique_ptr<ui::InteractionSequence::Step> step4 = |
| Tutorial::StepBuilder() |
| .SetAnchorElementID(kTestIdentifier1) |
| .SetTransitionOnlyOnEvent(true) |
| .Build(&service); |
| |
| builder.SetContext(kTestContext1) |
| .AddStep(std::move(step1)) |
| .AddStep(std::move(step2)) |
| .AddStep(std::move(step3)) |
| .AddStep(std::move(step4)) |
| .Build(); |
| } |
| |
| TEST_F(TutorialTest, TutorialRegistryRegistersTutorials) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| { |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone)); |
| description.can_be_restarted = true; |
| registry->AddTutorial(kTestTutorial1, std::move(description)); |
| } |
| |
| std::unique_ptr<HelpBubbleFactoryRegistry> bubble_factory_registry = |
| std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| registry->GetTutorialIdentifiers(); |
| } |
| |
| TEST_F(TutorialTest, SingleInteractionTutorialRuns) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| EXPECT_TRUE(service.currently_displayed_bubble()); |
| EXPECT_CALL_IN_SCOPE(completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble())); |
| } |
| |
| TEST_F(TutorialTest, TutorialWithCustomEvent) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kCustomEvent, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone, kCustomEventType1)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent( |
| &element_1, kCustomEventType1); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble())); |
| } |
| |
| TEST_F(TutorialTest, TutorialWithNamedElement) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| static constexpr char kElementName[] = "Element Name"; |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, std::string(), HelpBubbleArrow::kNone, |
| ui::CustomElementEventType(), |
| /* must_remain_visible =*/true, |
| /* transition_only_on_event =*/false, |
| base::BindLambdaForTesting( |
| [](ui::InteractionSequence* sequence, ui::TrackedElement* element) { |
| sequence->NameElement(element, base::StringPiece(kElementName)); |
| return true; |
| }))); |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| ui::ElementIdentifier(), kElementName, HelpBubbleArrow::kNone)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble())); |
| } |
| |
| TEST_F(TutorialTest, SingleStepRestartTutorial) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| ClickRestartButton(service.currently_displayed_bubble()); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble())); |
| } |
| |
| // Starts a tutorial with 3 steps, completes steps, then clicks restart tutorial |
| // then completes the tutorial again and closes it from the close button. |
| // Expects to call the completed callback. |
| TEST_F(TutorialTest, MultiStepRestartTutorialWithCloseOnComplete) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| ui::test::TestElement element_2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement element_3(kTestIdentifier3, kTestContext1); |
| |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone)); |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone)); |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier3, "", HelpBubbleArrow::kNone)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| element_2.Show(); |
| element_3.Show(); |
| |
| element_2.Hide(); |
| |
| ClickRestartButton(service.currently_displayed_bubble()); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| element_2.Show(); |
| |
| EXPECT_CALL_IN_SCOPE(completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble())); |
| } |
| |
| // Starts a tutorial with 3 steps, completes steps, then clicks restart tutorial |
| // then closes the tutorial on the first step. Expects to call the completed |
| // callback. |
| TEST_F(TutorialTest, MultiStepRestartTutorialWithDismissAfterRestart) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // build elements and keep them for triggering show/hide |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| ui::test::TestElement element_2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement element_3(kTestIdentifier3, kTestContext1); |
| |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone)); |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone)); |
| description.steps.emplace_back(TutorialDescription::Step( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier3, "", HelpBubbleArrow::kNone)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| element_2.Show(); |
| element_3.Show(); |
| |
| element_2.Hide(); |
| |
| ClickRestartButton(service.currently_displayed_bubble()); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| EXPECT_TRUE(service.currently_displayed_bubble() != nullptr); |
| |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, ClickDismissButton(service.currently_displayed_bubble())); |
| } |
| |
| } // namespace user_education |