| // Copyright 2021 The Chromium Authors |
| // 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/gtest_util.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/task_environment.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); |
| |
| const TutorialIdentifier kTestTutorial1{"kTestTutorial1"}; |
| const TutorialIdentifier kTestTutorial2{"kTestTutorial2"}; |
| const TutorialIdentifier kTestTutorial3{"kTestTutorial3"}; |
| |
| const char kHistogramName1[] = "histogram 1"; |
| const char kHistogramName2[] = "histogram 2"; |
| |
| 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 ClickNextButton(HelpBubble* bubble) { |
| auto* const help_bubble = static_cast<test::TestHelpBubble*>(bubble); |
| int button_index = help_bubble->GetIndexOfButtonWithText( |
| l10n_util::GetStringUTF16(IDS_TUTORIAL_NEXT_BUTTON)); |
| 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); |
| } |
| |
| bool HasButtonWithText(HelpBubble* bubble, int message_id) { |
| return static_cast<test::TestHelpBubble*>(bubble)->GetIndexOfButtonWithText( |
| l10n_util::GetStringUTF16(message_id)) != |
| test::TestHelpBubble::kNoButtonWithTextIndex; |
| } |
| |
| } // namespace |
| |
| class TutorialTest : public testing::Test { |
| public: |
| TutorialTest() = default; |
| ~TutorialTest() override = default; |
| |
| protected: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| }; |
| |
| 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, RegisterTutorial) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| { |
| TutorialDescription description; |
| description.steps.emplace_back( |
| 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, RegisterMultipleTutorials) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| TutorialDescription::Step step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone); |
| |
| TutorialDescription description1; |
| description1.steps.push_back(step); |
| description1.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| TutorialDescription description2; |
| description2.steps.push_back(step); |
| description2.histograms = MakeTutorialHistograms<kHistogramName2>(1); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description1)); |
| registry->AddTutorial(kTestTutorial2, std::move(description2)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial2)); |
| } |
| |
| TEST_F(TutorialTest, RegisterSameTutorialTwice) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| TutorialDescription::Step step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone); |
| |
| TutorialDescription description1; |
| description1.steps.push_back(step); |
| description1.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| TutorialDescription description2; |
| description2.steps.push_back(step); |
| description2.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description1)); |
| registry->AddTutorial(kTestTutorial1, std::move(description2)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorialsWithAndWithoutHistograms) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| TutorialDescription::Step step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone); |
| |
| TutorialDescription description1; |
| description1.steps.push_back(step); |
| |
| TutorialDescription description2; |
| description2.steps.push_back(step); |
| description2.histograms = MakeTutorialHistograms<kHistogramName2>(1); |
| |
| TutorialDescription description3; |
| description2.steps.push_back(step); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description1)); |
| registry->AddTutorial(kTestTutorial2, std::move(description2)); |
| registry->AddTutorial(kTestTutorial3, std::move(description3)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial2)); |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial3)); |
| } |
| |
| #if DCHECK_IS_ON() |
| #define MAYBE_RegisterDifferentTutorialsWithSameHistogram \ |
| RegisterDifferentTutorialsWithSameHistogram |
| #else |
| #define MAYBE_RegisterDifferentTutorialsWithSameHistogram \ |
| DISABLED_RegisterDifferentTutorialsWithSameHistogram |
| #endif |
| |
| TEST_F(TutorialTest, MAYBE_RegisterDifferentTutorialsWithSameHistogram) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| TutorialDescription::Step step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone); |
| |
| TutorialDescription description1; |
| description1.steps.push_back(step); |
| description1.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| TutorialDescription description2; |
| description2.steps.push_back(step); |
| description2.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description1)); |
| EXPECT_DCHECK_DEATH( |
| registry->AddTutorial(kTestTutorial2, std::move(description2))); |
| } |
| |
| TEST_F(TutorialTest, RegisterSameTutorialInMultipleRegistries) { |
| std::unique_ptr<TutorialRegistry> registry1 = |
| std::make_unique<TutorialRegistry>(); |
| std::unique_ptr<TutorialRegistry> registry2 = |
| std::make_unique<TutorialRegistry>(); |
| |
| TutorialDescription::Step step( |
| 0, IDS_OK, ui::InteractionSequence::StepType::kShown, kTestIdentifier1, |
| std::string(), HelpBubbleArrow::kNone); |
| |
| TutorialDescription description1; |
| description1.steps.push_back(step); |
| description1.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| TutorialDescription description2; |
| description2.steps.push_back(step); |
| description2.histograms = MakeTutorialHistograms<kHistogramName1>(1); |
| |
| registry1->AddTutorial(kTestTutorial1, std::move(description1)); |
| registry2->AddTutorial(kTestTutorial1, std::move(description2)); |
| EXPECT_TRUE(registry1->IsTutorialRegistered(kTestTutorial1)); |
| EXPECT_TRUE(registry2->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| 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(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_for_testing()); |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| TEST_F(TutorialTest, MultipleInteractionTutorialRuns) { |
| UNCALLED_MOCK_CALLBACK(TutorialDescription::NextButtonCallback, next); |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // Build and show test elements. |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| ui::test::TestElement element_2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement element_3(kTestIdentifier3, kTestContext1); |
| element_1.Show(); |
| element_2.Show(); |
| element_3.Show(); |
| |
| // Configure `next` callback to trigger `kCustomEventType1` on current anchor. |
| ON_CALL(next, Run).WillByDefault( |
| testing::Invoke([](ui::TrackedElement* current_anchor) { |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent( |
| current_anchor, kCustomEventType1); |
| })); |
| |
| // Build the tutorial `description`. |
| TutorialDescription description; |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK) |
| .AddDefaultNextButton()); |
| description.steps.emplace_back(TutorialDescription::EventStep( |
| kHelpBubbleNextButtonClickedEvent, kTestIdentifier1)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_OK) |
| .AddCustomNextButton(next.Get())); |
| description.steps.emplace_back( |
| TutorialDescription::EventStep(kCustomEventType1, kTestIdentifier2)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier3) |
| .SetBubbleBodyText(IDS_OK) |
| // Should no-op for last step. |
| .AddCustomNextButton(base::DoNothing()) |
| .AddDefaultNextButton()); |
| |
| // Register and start the tutorial. |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| // Verify only the default next button is visible and click it. |
| auto* bubble = service.currently_displayed_bubble_for_testing(); |
| ASSERT_TRUE(bubble); |
| EXPECT_FALSE(HasButtonWithText(bubble, IDS_TUTORIAL_CLOSE_TUTORIAL)); |
| EXPECT_TRUE(HasButtonWithText(bubble, IDS_TUTORIAL_NEXT_BUTTON)); |
| ClickNextButton(bubble); |
| |
| // Verify only the custom next button is visible and click it. |
| bubble = service.currently_displayed_bubble_for_testing(); |
| ASSERT_TRUE(bubble); |
| EXPECT_FALSE(HasButtonWithText(bubble, IDS_TUTORIAL_CLOSE_TUTORIAL)); |
| EXPECT_TRUE(HasButtonWithText(bubble, IDS_TUTORIAL_NEXT_BUTTON)); |
| EXPECT_CALL_IN_SCOPE(next, Run, ClickNextButton(bubble)); |
| |
| // Verify only the close button is visible and click it. |
| bubble = service.currently_displayed_bubble_for_testing(); |
| ASSERT_TRUE(bubble); |
| EXPECT_TRUE(HasButtonWithText(bubble, IDS_TUTORIAL_CLOSE_TUTORIAL)); |
| EXPECT_FALSE(HasButtonWithText(bubble, IDS_TUTORIAL_NEXT_BUTTON)); |
| EXPECT_CALL_IN_SCOPE(completed, Run, ClickCloseButton(bubble)); |
| } |
| |
| TEST_F(TutorialTest, StartTutorialAbortsExistingTutorial) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, aborted); |
| |
| 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. This has two steps, the second of which |
| // will not |
| TutorialDescription description; |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get()); |
| EXPECT_CALL_IN_SCOPE( |
| aborted, Run, service.StartTutorial(kTestTutorial1, element_1.context())); |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| } |
| |
| TEST_F(TutorialTest, StartTutorialCompletesExistingTutorial) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, aborted); |
| |
| 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. This has two steps, the second of which |
| // will not |
| TutorialDescription description; |
| description.steps.emplace_back(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(), |
| aborted.Get()); |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| service.StartTutorial(kTestTutorial1, element_1.context())); |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| } |
| |
| 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( |
| 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_for_testing())); |
| } |
| |
| TEST_F(TutorialTest, TutorialWithNamedElement) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| static constexpr char kElementName1[] = "Element Name 1"; |
| static constexpr char kElementName2[] = "Element Name 2"; |
| static constexpr char kElementName3[] = "Element Name 3"; |
| |
| 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( |
| 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(kElementName1)); |
| return true; |
| })); |
| description.steps.emplace_back( |
| TutorialDescription::HiddenStep::WaitForShown(kElementName1) |
| .NameElement(kElementName2)); |
| description.steps.emplace_back( |
| TutorialDescription::HiddenStep::WaitForShown(kElementName2) |
| .NameElements(base::BindRepeating( |
| [](ui::InteractionSequence* sequence, ui::TrackedElement* el) { |
| sequence->NameElement(el, base::StringPiece(kElementName3)); |
| return true; |
| }))); |
| description.steps.emplace_back( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| ui::ElementIdentifier(), kElementName3, 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_for_testing())); |
| } |
| |
| 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(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_for_testing()); |
| |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| // 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(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(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_for_testing()); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| element_2.Show(); |
| |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| // 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(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(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_for_testing()); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing() != nullptr); |
| |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| ClickDismissButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| // Verify that when the final bubble of a tutorial is forced to close without |
| // being dismissed by the user (e.g. because its anchor element disappears, or |
| // it's programmatically closed) the tutorial ends. |
| TEST_F(TutorialTest, BubbleClosingProgrammaticallyOnlyEndsTutorialOnLastStep) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| |
| 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); |
| |
| element_1.Show(); |
| |
| // Build the tutorial Description |
| TutorialDescription description; |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get()); |
| service.currently_displayed_bubble_for_testing()->Close(); |
| element_2.Show(); |
| EXPECT_CALL_IN_SCOPE( |
| completed, Run, |
| service.currently_displayed_bubble_for_testing()->Close()); |
| } |
| |
| TEST_F(TutorialTest, TimeoutBeforeFirstBubble) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| ui::test::TestElement el(kTestIdentifier1, kTestContext1); |
| |
| TutorialDescription description; |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el.context(), completed.Get(), |
| aborted.Get()); |
| EXPECT_FALSE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_CALL_IN_SCOPE(aborted, Run, |
| task_environment_.FastForwardUntilNoTasksRemain()); |
| } |
| |
| TEST_F(TutorialTest, TimeoutBetweenBubbles) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| ui::test::TestElement el1(kTestIdentifier1, kTestContext1); |
| ui::test::TestElement el2(kTestIdentifier1, kTestContext1); |
| el1.Show(); |
| |
| TutorialDescription description; |
| description.steps.emplace_back( |
| IDS_OK, IDS_OK, ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone, |
| ui::CustomElementEventType(), /* must_remain_visible */ false); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(), |
| aborted.Get()); |
| |
| // This closes the bubble but does not advance the tutorial. |
| el1.Hide(); |
| EXPECT_FALSE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_CALL_IN_SCOPE(aborted, Run, |
| task_environment_.FastForwardUntilNoTasksRemain()); |
| } |
| |
| TEST_F(TutorialTest, NoTimeoutIfBubbleShowing) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| ui::test::TestElement el1(kTestIdentifier1, kTestContext1); |
| ui::test::TestElement el2(kTestIdentifier1, kTestContext1); |
| el1.Show(); |
| |
| TutorialDescription description; |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier1, "", HelpBubbleArrow::kNone); |
| description.steps.emplace_back(IDS_OK, IDS_OK, |
| ui::InteractionSequence::StepType::kShown, |
| kTestIdentifier2, "", HelpBubbleArrow::kNone); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(), |
| aborted.Get()); |
| |
| // Since there is a bubble, there is no timeout. |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| |
| // When we exit and destroy the service, the callback will be called. |
| EXPECT_CALL(aborted, Run).Times(1); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorialWithCreate) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| { |
| auto description = TutorialDescription::Create<kHistogramName1>( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| EXPECT_TRUE(description.steps.size() == 1); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description)); |
| } |
| |
| std::unique_ptr<HelpBubbleFactoryRegistry> bubble_factory_registry = |
| std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorialWithCreateFromVector) { |
| std::unique_ptr<TutorialRegistry> registry = |
| std::make_unique<TutorialRegistry>(); |
| |
| { |
| TutorialDescription::Step first_step = |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK); |
| |
| std::vector<TutorialDescription::Step> next_steps = { |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_OK), |
| TutorialDescription::BubbleStep(kTestIdentifier3) |
| .SetBubbleBodyText(IDS_OK)}; |
| |
| auto description = |
| TutorialDescription::Create<kHistogramName1>(first_step, next_steps); |
| description.can_be_restarted = true; |
| EXPECT_TRUE(description.steps.size() == 3); |
| |
| registry->AddTutorial(kTestTutorial1, std::move(description)); |
| } |
| |
| std::unique_ptr<HelpBubbleFactoryRegistry> bubble_factory_registry = |
| std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| } // namespace user_education |