| // 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/tutorial.h" |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/run_loop.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_run_loop_timeout.h" |
| #include "base/test/task_environment.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/user_education/common/help_bubble/help_bubble_factory_registry.h" |
| #include "components/user_education/common/help_bubble/help_bubble_params.h" |
| #include "components/user_education/common/tutorial/tutorial_description.h" |
| #include "components/user_education/common/tutorial/tutorial_identifier.h" |
| #include "components/user_education/common/tutorial/tutorial_registry.h" |
| #include "components/user_education/common/tutorial/tutorial_service.h" |
| #include "components/user_education/common/user_education_events.h" |
| #include "components/user_education/test/test_help_bubble.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/interaction/interaction_sequence_test_util.h" |
| #include "ui/base/interaction/interactive_test.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_ELEMENT_IDENTIFIER_VALUE(kTestIdentifier4); |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kCustomEventType1); |
| |
| const char kTestElementName1[] = "ELEMENT_NAME_1"; |
| |
| constexpr auto kTestContext1 = |
| ui::ElementContext::CreateFakeContextForTesting(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(); |
| } |
| }; |
| |
| class ScopedTestTutorialState : public ScopedTutorialState { |
| public: |
| explicit ScopedTestTutorialState(ui::test::TestElement* element) |
| : ScopedTutorialState(element->context()), element_(element) { |
| element_->Show(); |
| } |
| ~ScopedTestTutorialState() override { element_->Hide(); } |
| |
| private: |
| raw_ptr<ui::test::TestElement> element_; |
| }; |
| |
| 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; |
| |
| void ClearEventQueue(bool fast_forward = true) { |
| if (fast_forward) { |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| } else { |
| base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| } |
| |
| private: |
| 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; |
| int current_progress = 0; |
| |
| // build a step with an ElementID |
| auto step1 = Tutorial::Builder::BuildFromDescriptionStep( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK), |
| 2, current_progress, false, false, IDS_TUTORIAL_CLOSE_TUTORIAL, &service); |
| |
| // build a step that names an element |
| auto step2 = Tutorial::Builder::BuildFromDescriptionStep( |
| TutorialDescription::HiddenStep::WaitForShown(kTestIdentifier1) |
| .NameElement(kTestElementName1), |
| 2, current_progress, false, false, IDS_TUTORIAL_CLOSE_TUTORIAL, &service); |
| |
| // build a step with a named element |
| auto step3 = Tutorial::Builder::BuildFromDescriptionStep( |
| TutorialDescription::BubbleStep(kTestElementName1) |
| .SetBubbleBodyText(IDS_OK), |
| 2, current_progress, false, false, IDS_TUTORIAL_CLOSE_TUTORIAL, &service); |
| |
| // transition event |
| auto step4 = Tutorial::Builder::BuildFromDescriptionStep( |
| TutorialDescription::HiddenStep::WaitForShowEvent(kTestIdentifier1), 2, |
| current_progress, false, false, IDS_TUTORIAL_CLOSE_TUTORIAL, &service); |
| |
| // final bubble |
| auto step5 = Tutorial::Builder::BuildFromDescriptionStep( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK), |
| 2, current_progress, true, false, IDS_TUTORIAL_CLOSE_TUTORIAL, &service); |
| |
| builder.SetContext(kTestContext1) |
| .AddStep(std::move(step1)) |
| .AddStep(std::move(step2)) |
| .AddStep(std::move(step3)) |
| .AddStep(std::move(step4)) |
| .AddStep(std::move(step5)) |
| .Build(); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorial) { |
| auto registry = std::make_unique<TutorialRegistry>(); |
| |
| { |
| TutorialDescription description; |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry->AddTutorial(kTestTutorial1, std::move(description)); |
| } |
| |
| auto bubble_factory_registry = std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| registry->GetTutorialIdentifiers(); |
| } |
| |
| TEST_F(TutorialTest, RegisterMultipleTutorials) { |
| auto registry = std::make_unique<TutorialRegistry>(); |
| |
| const auto step = TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK); |
| |
| 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) { |
| auto registry = std::make_unique<TutorialRegistry>(); |
| |
| const auto step = TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK); |
| |
| 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) { |
| auto registry = std::make_unique<TutorialRegistry>(); |
| |
| const auto step = TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK); |
| |
| 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)); |
| } |
| |
| TEST_F(TutorialTest, RegisterSameTutorialInMultipleRegistries) { |
| auto registry1 = std::make_unique<TutorialRegistry>(); |
| auto registry2 = std::make_unique<TutorialRegistry>(); |
| |
| const auto step = TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| ClearEventQueue(); |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_ASYNC_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([](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()); |
| ClearEventQueue(); |
| |
| // 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)); |
| EXPECT_EQ(&element_1, bubble->AsA<test::TestHelpBubble>()->anchor_element()); |
| ClickNextButton(bubble); |
| ClearEventQueue(); |
| |
| // 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_EQ(&element_2, bubble->AsA<test::TestHelpBubble>()->anchor_element()); |
| EXPECT_CALL_IN_SCOPE(next, Run, ClickNextButton(bubble)); |
| ClearEventQueue(); |
| |
| // 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_ASYNC_CALL_IN_SCOPE(completed, Run, ClickCloseButton(bubble)); |
| } |
| |
| TEST_F(TutorialTest, StartTutorialAbortsExistingTutorial) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| EXPECT_ASYNC_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::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| EXPECT_ASYNC_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( |
| TutorialDescription::EventStep(kCustomEventType1, kTestIdentifier1)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent( |
| &element_1, kCustomEventType1); |
| |
| EXPECT_ASYNC_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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK) |
| .NameElement(kElementName1)); |
| 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, std::string_view(kElementName3)); |
| return true; |
| }))); |
| description.steps.emplace_back(TutorialDescription::BubbleStep(kElementName3) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| TEST_F(TutorialTest, TutorialWithExtendedProperties) { |
| 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 element. |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Configure extended properties. |
| HelpBubbleParams::ExtendedProperties extended_properties; |
| extended_properties.values().Set("string", "v1"); |
| extended_properties.values().Set("bool", true); |
| extended_properties.values().Set("int", 1); |
| |
| // Build the tutorial `description`. |
| TutorialDescription description; |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK) |
| .SetExtendedProperties(extended_properties)); |
| |
| // Register and start the tutorial. |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| ClearEventQueue(); |
| |
| // Verify the bubble has been forwarded the extended properties. |
| auto* bubble = service.currently_displayed_bubble_for_testing(); |
| ASSERT_TRUE(bubble); |
| EXPECT_THAT( |
| static_cast<test::TestHelpBubble*>(bubble)->params().extended_properties, |
| extended_properties); |
| |
| // Close the bubble to complete the tutorial. |
| EXPECT_ASYNC_CALL_IN_SCOPE(completed, Run, ClickCloseButton(bubble)); |
| } |
| |
| TEST_F(TutorialTest, SingleStepRestartTutorial) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| restarted, Run, |
| ClickRestartButton(service.currently_displayed_bubble_for_testing())); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| completed, Run, |
| ClickCloseButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| // Clicks restart tutorial a few times. Then closes the tutorial from the close |
| // button. Expects to call the restarted callback multiple times. |
| TEST_F(TutorialTest, SingleStepRestartTutorialCanRestartMultipleTimes) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| |
| constexpr int kRestartedTimes = 3; |
| for (int i = 0; i < kRestartedTimes; ++i) { |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| restarted, Run, |
| ClickRestartButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| EXPECT_ASYNC_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); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier3) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| element_2.Show(); |
| ClearEventQueue(); |
| element_3.Show(); |
| ClearEventQueue(); |
| element_2.Hide(); |
| ClearEventQueue(); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| restarted, Run, |
| ClickRestartButton(service.currently_displayed_bubble_for_testing())); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| element_2.Show(); |
| |
| EXPECT_ASYNC_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); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier3) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| element_2.Show(); |
| ClearEventQueue(); |
| element_3.Show(); |
| ClearEventQueue(); |
| element_2.Hide(); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| restarted, Run, |
| ClickRestartButton(service.currently_displayed_bubble_for_testing())); |
| ClearEventQueue(); |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| completed, Run, |
| ClickDismissButton(service.currently_displayed_bubble_for_testing())); |
| } |
| |
| // Starts a tutorial with 3 steps. Completes steps and clicks restart tutorial a |
| // few times. Then closes the tutorial on the first step. Expects to call the |
| // restarted callback multiple times. |
| TEST_F(TutorialTest, MultiStepRestartTutorialCanRestartMultipleTimes) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier3) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| |
| constexpr int kRestartedTimes = 3; |
| for (int i = 0; i < kRestartedTimes; ++i) { |
| element_2.Show(); |
| ClearEventQueue(); |
| element_3.Show(); |
| ClearEventQueue(); |
| element_2.Hide(); |
| EXPECT_ASYNC_CALL_IN_SCOPE( |
| restarted, Run, |
| ClickRestartButton(service.currently_displayed_bubble_for_testing())); |
| ClearEventQueue(); |
| } |
| |
| EXPECT_TRUE(service.IsRunningTutorial()); |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| |
| EXPECT_ASYNC_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); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.can_be_restarted = true; |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| element_2.Show(); |
| ClearEventQueue(); |
| EXPECT_ASYNC_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); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| EXPECT_FALSE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_ASYNC_CALL_IN_SCOPE(aborted, Run, ClearEventQueue()); |
| } |
| |
| TEST_F(TutorialTest, TimeoutBetweenBubbles) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK) |
| .AbortIfVisibilityLost(false)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| |
| // This closes the bubble but does not advance the tutorial. |
| el1.Hide(); |
| ClearEventQueue(/*fast_forward=*/false); |
| EXPECT_FALSE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_ASYNC_CALL_IN_SCOPE(aborted, Run, ClearEventQueue()); |
| } |
| |
| TEST_F(TutorialTest, NoTimeoutIfBubbleShowing) { |
| UNCALLED_MOCK_CALLBACK(TutorialService::CompletedCallback, completed); |
| UNCALLED_MOCK_CALLBACK(TutorialService::AbortedCallback, aborted); |
| UNCALLED_MOCK_CALLBACK(TutorialService::RestartedCallback, restarted); |
| |
| 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( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier2) |
| .SetBubbleTitleText(IDS_OK) |
| .SetBubbleBodyText(IDS_OK)); |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| |
| service.StartTutorial(kTestTutorial1, el1.context(), completed.Get(), |
| aborted.Get(), restarted.Get()); |
| ClearEventQueue(); |
| |
| // Since there is a bubble, there is no timeout. |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| ClearEventQueue(); |
| |
| // When we exit and destroy the service, the callback will be called. |
| EXPECT_CALL(aborted, Run).Times(1); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorialWithCreate) { |
| auto 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)); |
| } |
| |
| auto bubble_factory_registry = std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| TEST_F(TutorialTest, RegisterTutorialWithCreateFromVector) { |
| auto 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)); |
| } |
| |
| auto bubble_factory_registry = std::make_unique<HelpBubbleFactoryRegistry>(); |
| |
| EXPECT_TRUE(registry->IsTutorialRegistered(kTestTutorial1)); |
| } |
| |
| TEST_F(TutorialTest, SetupTemporaryStateCallback) { |
| 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 element. |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Create another test element which will be shown during the tutorial. |
| ui::test::TestElement element_2(kTestIdentifier2, kTestContext1); |
| ASSERT_FALSE(element_2.IsVisible()); |
| |
| // Build the tutorial `description`. |
| TutorialDescription description; |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK)); |
| description.temporary_state_callback = base::BindRepeating( |
| [](ui::test::TestElement* element, |
| ui::ElementContext context) -> std::unique_ptr<ScopedTutorialState> { |
| return base::WrapUnique(new ScopedTestTutorialState(element)); |
| }, |
| base::Unretained(&element_2)); |
| |
| // Register and start the tutorial. |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| service.StartTutorial(kTestTutorial1, element_1.context(), completed.Get()); |
| ClearEventQueue(); |
| |
| auto* bubble = service.currently_displayed_bubble_for_testing(); |
| // Verify that the element is shown when the tutorial is active. |
| ASSERT_TRUE(element_2.IsVisible()); |
| // Close the bubble to complete the tutorial. |
| EXPECT_ASYNC_CALL_IN_SCOPE(completed, Run, ClickCloseButton(bubble)); |
| // Verify that the element is hidden when the tutorial is completed. |
| ASSERT_FALSE(element_2.IsVisible()); |
| } |
| |
| TEST_F(TutorialTest, CleanupTemporaryStateOnAbort) { |
| const auto bubble_factory_registry = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TutorialRegistry registry; |
| TestTutorialService service(®istry, bubble_factory_registry.get()); |
| |
| // Build and show test element. |
| ui::test::TestElement element_1(kTestIdentifier1, kTestContext1); |
| element_1.Show(); |
| |
| // Create another test element which will be shown during the tutorial. |
| ui::test::TestElement element_2(kTestIdentifier2, kTestContext1); |
| ASSERT_FALSE(element_2.IsVisible()); |
| |
| // Build the tutorial `description`. |
| TutorialDescription description; |
| description.steps.emplace_back( |
| TutorialDescription::BubbleStep(kTestIdentifier1) |
| .SetBubbleBodyText(IDS_OK)); |
| description.temporary_state_callback = base::BindRepeating( |
| [](ui::test::TestElement* element, |
| ui::ElementContext context) -> std::unique_ptr<ScopedTutorialState> { |
| return base::WrapUnique(new ScopedTestTutorialState(element)); |
| }, |
| base::Unretained(&element_2)); |
| |
| // Register and start the tutorial. |
| registry.AddTutorial(kTestTutorial1, std::move(description)); |
| service.StartTutorial(kTestTutorial1, element_1.context()); |
| ClearEventQueue(); |
| |
| // Verify that the bubble is shown to the user. |
| EXPECT_TRUE(service.currently_displayed_bubble_for_testing()); |
| // Verify that the element is shown when the tutorial is active. |
| ASSERT_TRUE(element_2.IsVisible()); |
| |
| // Verify the tutorial is aborted when the anchor visibility is lost. |
| element_1.Hide(); |
| ClearEventQueue(); |
| EXPECT_FALSE(service.currently_displayed_bubble_for_testing()); |
| EXPECT_FALSE(service.IsRunningTutorial()); |
| // Verify that the state is reset when the tutorial is aborted. |
| ASSERT_FALSE(element_2.IsVisible()); |
| } |
| |
| // Test where the parameter is a bitfield describing choices the test will make |
| // at each branch. |
| class ConditionalTutorialTest |
| : public ui::test::InteractiveTestMixin<TutorialTest>, |
| public testing::WithParamInterface<int> { |
| public: |
| ConditionalTutorialTest() = default; |
| ~ConditionalTutorialTest() override = default; |
| |
| void SetUp() override { |
| InteractiveTestMixin<TutorialTest>::SetUp(); |
| EXPECT_CALL(completed_, Run).Times(1); |
| first_anchor_.Show(); |
| private_test_impl().set_default_context(first_anchor_.context()); |
| } |
| |
| protected: |
| using BubbleStep = TutorialDescription::BubbleStep; |
| using IfStep = TutorialDescription::If; |
| |
| // Gets whether the `n`th branch should be active. |
| bool GetBranchValue(int n) const { return 0 != (GetParam() & (1 << n)); } |
| |
| // Gets the condition function for the `n`th branch. |
| TutorialDescription::ConditionalCallback Branch(int n) const { |
| const bool result = GetBranchValue(n); |
| return base::BindLambdaForTesting( |
| [result](const ui::TrackedElement* el) { return result; }); |
| } |
| |
| template <typename... Args> |
| auto StartTutorial(Args... steps) { |
| tutorial_registry_.AddTutorial( |
| kTestTutorial1, TutorialDescription::Create<kHistogramName1>(steps...)); |
| return Do([this]() { |
| tutorial_service_.StartTutorial(kTestTutorial1, first_anchor_.context(), |
| completed_.Get(), aborted_.Get()); |
| }); |
| } |
| |
| // Closes a help bubble. The bubble must already be visible. |
| auto CloseHelpBubble() { |
| return Steps(WithElement(test::TestHelpBubble::kElementId, |
| [](ui::TrackedElement* el) { |
| el->AsA<test::TestHelpBubbleElement>() |
| ->bubble() |
| ->SimulateDismiss(); |
| }), |
| WaitForHide(test::TestHelpBubble::kElementId)); |
| } |
| |
| auto VerifyHelpBubble(std::map<int, int> expected_strings, |
| std::optional<std::pair<int, int>> progress) { |
| const int id = expected_strings.size() == 1U |
| ? expected_strings.begin()->second |
| : expected_strings[GetParam()]; |
| auto steps = |
| Steps(CheckElement( |
| test::TestHelpBubble::kElementId, |
| [](ui::TrackedElement* el) { |
| return el->AsA<test::TestHelpBubbleElement>() |
| ->bubble() |
| ->params() |
| .body_text; |
| }, |
| l10n_util::GetStringUTF16(id)), |
| CheckElement( |
| test::TestHelpBubble::kElementId, |
| [](ui::TrackedElement* el) { |
| return el->AsA<test::TestHelpBubbleElement>() |
| ->bubble() |
| ->params() |
| .buttons.empty(); |
| }, |
| progress.has_value()), |
| CheckElement( |
| test::TestHelpBubble::kElementId, |
| [](ui::TrackedElement* el) { |
| return el->AsA<test::TestHelpBubbleElement>() |
| ->bubble() |
| ->params() |
| .progress; |
| }, |
| progress) |
| .SetMustRemainVisible(false)); |
| const std::string progress_str = |
| progress.has_value() |
| ? base::StringPrintf("{%d, %d}", progress->first, progress->second) |
| : std::string("<null>"); |
| AddDescriptionPrefix(steps, base::StringPrintf("VerifyHelpBubble( %d, %s )", |
| id, progress_str.c_str())); |
| return steps; |
| } |
| |
| TutorialRegistry tutorial_registry_; |
| std::unique_ptr<HelpBubbleFactoryRegistry> help_bubble_registry_ = |
| CreateTestTutorialBubbleFactoryRegistry(); |
| TestTutorialService tutorial_service_{&tutorial_registry_, |
| help_bubble_registry_.get()}; |
| base::test::ScopedRunLoopTimeout timeout_{FROM_HERE, base::Seconds(30)}; |
| testing::StrictMock<base::MockCallback<TutorialService::CompletedCallback>> |
| completed_; |
| testing::StrictMock<base::MockCallback<TutorialService::AbortedCallback>> |
| aborted_; |
| ui::test::TestElement first_anchor_{kTestIdentifier1, kTestContext1}; |
| }; |
| |
| using ConditionalTutorialTest1 = ConditionalTutorialTest; |
| INSTANTIATE_TEST_SUITE_P(, ConditionalTutorialTest1, testing::Range(0, 2)); |
| |
| TEST_P(ConditionalTutorialTest1, ConditionalAtStartOfTutorial) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| IfStep(kTestIdentifier1, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_OK)) |
| .Else(BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_CANCEL)), |
| BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_CLEAR)), |
| VerifyHelpBubble({{0, IDS_CANCEL}, {1, IDS_OK}}, std::make_pair(1, 1)), |
| Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CLEAR}}, std::nullopt), CloseHelpBubble()); |
| } |
| |
| TEST_P(ConditionalTutorialTest1, ConditionalInMiddleOfTutorial) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement el3(kTestIdentifier3, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_DONE), |
| IfStep(kTestIdentifier2, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_OK)) |
| .Else(BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_CANCEL)), |
| BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_CLEAR)), |
| VerifyHelpBubble({{-1, IDS_DONE}}, std::make_pair(1, 2)), |
| Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{0, IDS_CANCEL}, {1, IDS_OK}}, std::make_pair(2, 2)), |
| Do([&]() { el3.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CLEAR}}, std::nullopt), CloseHelpBubble()); |
| } |
| |
| TEST_P(ConditionalTutorialTest1, ConditionalAtEndOfTutorial) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_DONE), |
| IfStep(kTestIdentifier2, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_OK)) |
| .Else( |
| BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_CANCEL))), |
| VerifyHelpBubble({{-1, IDS_DONE}}, std::make_pair(1, 1)), |
| Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{0, IDS_CANCEL}, {1, IDS_OK}}, std::nullopt), |
| CloseHelpBubble()); |
| } |
| |
| TEST_P(ConditionalTutorialTest1, ConditionalAtEndOfTutorialUnevenSteps) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement el3(kTestIdentifier3, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_DONE), |
| IfStep(kTestIdentifier2, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_OK)) |
| .Else( |
| BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_CLEAR), |
| BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_CANCEL))), |
| VerifyHelpBubble({{-1, IDS_DONE}}, std::make_pair(1, 2)), |
| Do([&]() { el2.Show(); }), |
| If([this]() { return !GetBranchValue(0); }, |
| Then(WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CLEAR}}, std::make_pair(2, 2)))), |
| Do([&]() { el3.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{0, IDS_CANCEL}, {1, IDS_OK}}, std::nullopt), |
| CloseHelpBubble()); |
| } |
| |
| TEST_P(ConditionalTutorialTest1, OptionalStep) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement el3(kTestIdentifier3, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_DONE), |
| IfStep(kTestIdentifier2, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_OK)), |
| BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_CLEAR)), |
| VerifyHelpBubble({{-1, IDS_DONE}}, std::make_pair(1, 2)), |
| If([this]() { return GetBranchValue(0); }, |
| Then(Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{1, IDS_OK}}, std::make_pair(2, 2)))), |
| Do([&]() { el3.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CLEAR}}, std::nullopt), CloseHelpBubble()); |
| } |
| |
| TEST_P(ConditionalTutorialTest1, WaitForAnyOf) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement el3(kTestIdentifier3, kTestContext1); |
| ui::test::TestElement el4(kTestIdentifier4, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_DONE), |
| TutorialDescription::WaitForAnyOf(kTestIdentifier2) |
| .Or(kTestIdentifier3), |
| IfStep(kTestIdentifier2) |
| .Then(BubbleStep(kTestIdentifier2).SetBubbleBodyText(IDS_OK)) |
| .Else(BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_CANCEL)), |
| BubbleStep(kTestIdentifier4).SetBubbleBodyText(IDS_CLEAR)), |
| VerifyHelpBubble({{-1, IDS_DONE}}, std::make_pair(1, 2)), |
| If([this]() { return GetBranchValue(0); }, |
| Then(Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_OK}}, std::make_pair(2, 2))), |
| Else(Do([&]() { el3.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CANCEL}}, std::make_pair(2, 2)))), |
| Do([&]() { el4.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_CLEAR}}, std::nullopt), CloseHelpBubble()); |
| } |
| |
| using ConditionalTutorialTest2 = ConditionalTutorialTest; |
| INSTANTIATE_TEST_SUITE_P(, ConditionalTutorialTest2, testing::Range(0, 4)); |
| |
| TEST_P(ConditionalTutorialTest2, NestedConditionals) { |
| ui::test::TestElement el2(kTestIdentifier2, kTestContext1); |
| ui::test::TestElement el3(kTestIdentifier3, kTestContext1); |
| |
| RunTestSequence( |
| StartTutorial( |
| IfStep(kTestIdentifier1, Branch(0)) |
| .Then(BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_OK), |
| IfStep(kTestIdentifier2, Branch(1)) |
| .Then(BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_DONE)) |
| .Else(BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_CLEAR))) |
| .Else(BubbleStep(kTestIdentifier1).SetBubbleBodyText(IDS_CANCEL), |
| IfStep(kTestIdentifier2, Branch(1)) |
| .Then(BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_ADD)) |
| .Else(BubbleStep(kTestIdentifier2) |
| .SetBubbleBodyText(IDS_REMOVE))), |
| BubbleStep(kTestIdentifier3).SetBubbleBodyText(IDS_SAVE)), |
| VerifyHelpBubble( |
| {{0, IDS_CANCEL}, {1, IDS_OK}, {2, IDS_CANCEL}, {3, IDS_OK}}, |
| std::make_pair(1, 2)), |
| Do([&]() { el2.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble( |
| {{0, IDS_REMOVE}, {1, IDS_CLEAR}, {2, IDS_ADD}, {3, IDS_DONE}}, |
| std::make_pair(2, 2)), |
| Do([&]() { el3.Show(); }), |
| WaitForShow(test::TestHelpBubble::kElementId) |
| .SetTransitionOnlyOnEvent(true), |
| VerifyHelpBubble({{-1, IDS_SAVE}}, std::nullopt), CloseHelpBubble()); |
| } |
| |
| } // namespace user_education |