| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/interaction/interactive_views_test.h" |
| |
| #include <functional> |
| #include <memory> |
| #include <string> |
| |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/mock_callback.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/interaction/element_tracker.h" |
| #include "ui/base/interaction/expect_call_in_scope.h" |
| #include "ui/base/interaction/state_observer.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/tabbed_pane/tabbed_pane.h" |
| #include "ui/views/interaction/polling_view_observer.h" |
| #include "ui/views/layout/flex_layout_view.h" |
| #include "ui/views/layout/layout_types.h" |
| #include "ui/views/test/widget_test.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/view_utils.h" |
| |
| namespace views::test { |
| |
| namespace { |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kContentsId); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kButtonsId); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kButton1Id); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTabbedPaneId); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kScrollChild1Id); |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kScrollChild2Id); |
| constexpr char16_t kButton1Caption[] = u"Button 1"; |
| constexpr char16_t kButton2Caption[] = u"Button 2"; |
| constexpr char16_t kTab1Title[] = u"Tab 1"; |
| constexpr char16_t kTab2Title[] = u"Tab 2"; |
| constexpr char16_t kTab3Title[] = u"Tab 3"; |
| constexpr char16_t kTab1Contents[] = u"Tab 1 Contents"; |
| constexpr char16_t kTab2Contents[] = u"Tab 2 Contents"; |
| constexpr char16_t kTab3Contents[] = u"Tab 3 Contents"; |
| constexpr char kViewName[] = "Named View"; |
| constexpr char kViewName2[] = "Second Named View"; |
| } // namespace |
| |
| class InteractiveViewsTestTest : public InteractiveViewsTest { |
| public: |
| InteractiveViewsTestTest() = default; |
| ~InteractiveViewsTestTest() override = default; |
| |
| void SetUp() override { |
| InteractiveViewsTest::SetUp(); |
| |
| // Set up the Views hierarchy to use for the tests. |
| auto contents = |
| Builder<FlexLayoutView>() |
| .SetProperty(kElementIdentifierKey, kContentsId) |
| .SetOrientation(LayoutOrientation::kVertical) |
| .AddChildren( |
| Builder<TabbedPane>() |
| .CopyAddressTo(&tabs_) |
| .SetProperty(kElementIdentifierKey, kTabbedPaneId) |
| .AddTab(kTab1Title, std::make_unique<Label>(kTab1Contents), |
| nullptr) |
| .AddTab(kTab2Title, std::make_unique<Label>(kTab2Contents), |
| nullptr) |
| .AddTab(kTab3Title, std::make_unique<Label>(kTab3Contents), |
| nullptr), |
| Builder<FlexLayoutView>() |
| .SetProperty(kElementIdentifierKey, kButtonsId) |
| .SetOrientation(LayoutOrientation::kHorizontal) |
| .AddChildren( |
| Builder<LabelButton>() |
| .CopyAddressTo(&button1_) |
| .SetProperty(kElementIdentifierKey, kButton1Id) |
| .SetText(kButton1Caption) |
| .SetCallback(button1_callback_.Get()), |
| Builder<LabelButton>() |
| .CopyAddressTo(&button2_) |
| .SetText(kButton2Caption) |
| .SetCallback(button2_callback_.Get())), |
| Builder<ScrollView>() |
| .CopyAddressTo(&scroll_) |
| .SetPreferredSize(gfx::Size(100, 90)) |
| .SetVerticalScrollBarMode( |
| ScrollView::ScrollBarMode::kEnabled) |
| .SetContents( |
| Builder<FlexLayoutView>() |
| .SetOrientation(LayoutOrientation::kVertical) |
| .SetSize(gfx::Size(100, 200)) |
| .AddChildren( |
| Builder<View>() |
| .SetProperty(kElementIdentifierKey, |
| kScrollChild1Id) |
| .SetPreferredSize(gfx::Size(100, 100)), |
| Builder<View>() |
| .SetProperty(kElementIdentifierKey, |
| kScrollChild2Id) |
| .SetPreferredSize(gfx::Size(100, 100))))); |
| |
| // Create and show the test widget. |
| widget_ = CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| widget_->SetContentsView(std::move(contents).Build()); |
| WidgetVisibleWaiter waiter(widget_.get()); |
| widget_->Show(); |
| waiter.Wait(); |
| widget_->LayoutRootViewIfNecessary(); |
| |
| // This is required before RunTestSequence() can be called. |
| SetContextWidget(widget_.get()); |
| } |
| |
| void TearDown() override { |
| SetContextWidget(nullptr); |
| tabs_ = nullptr; |
| button1_ = nullptr; |
| button2_ = nullptr; |
| scroll_ = nullptr; |
| widget_.reset(); |
| InteractiveViewsTest::TearDown(); |
| } |
| |
| static void DoPost(base::OnceClosure closure) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(closure)); |
| } |
| |
| auto Post(base::OnceClosure closure) { |
| return Do(base::BindOnce( |
| [](base::OnceClosure closure) { DoPost(std::move(closure)); }, |
| std::move(closure))); |
| } |
| |
| protected: |
| using ButtonCallbackMock = testing::StrictMock< |
| base::MockCallback<Button::PressedCallback::Callback>>; |
| |
| std::unique_ptr<Widget> widget_; |
| raw_ptr<TabbedPane> tabs_; |
| raw_ptr<LabelButton> button1_; |
| raw_ptr<LabelButton> button2_; |
| raw_ptr<ScrollView> scroll_; |
| ButtonCallbackMock button1_callback_; |
| ButtonCallbackMock button2_callback_; |
| }; |
| |
| TEST_F(InteractiveViewsTestTest, WithView) { |
| RunTestSequence(WithView(kButton1Id, base::BindOnce([](LabelButton* button) { |
| EXPECT_TRUE(button->GetVisible()); |
| })), |
| // Check version with arbitrary return value and matcher. |
| WithView(kTabbedPaneId, base::BindOnce([](TabbedPane* tabs) { |
| EXPECT_EQ(3U, tabs->GetTabCount()); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, CheckView) { |
| RunTestSequence( |
| // Check version with no matcher and only boolean return value. |
| CheckView(kButton1Id, base::BindOnce([](LabelButton* button) { |
| return button->GetVisible(); |
| })), |
| // Check version with arbitrary return value and matcher. |
| CheckView(kTabbedPaneId, base::BindOnce([](TabbedPane* tabs) { |
| return tabs->GetTabCount(); |
| }), |
| testing::Gt(2U))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, CheckViewFails) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| private_test_impl().set_aborted_callback_for_testing(aborted.Get()); |
| EXPECT_CALL_IN_SCOPE( |
| aborted, Run, |
| RunTestSequence( |
| // Check version with no matcher and only boolean return value. |
| CheckView(kButton1Id, base::BindOnce([](LabelButton* button) { |
| return !button->GetVisible(); |
| })))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, CheckViewProperty) { |
| RunTestSequence( |
| CheckViewProperty(kButton1Id, &LabelButton::GetText, |
| // Implicit creation of an equality matcher. |
| kButton1Caption), |
| CheckViewProperty(kTabbedPaneId, &TabbedPane::GetSelectedTabIndex, |
| // Explicit creation of an inequality matcher. |
| testing::Ne(1U))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, CheckViewPropertyFails) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| private_test_impl().set_aborted_callback_for_testing(aborted.Get()); |
| EXPECT_CALL_IN_SCOPE( |
| aborted, Run, |
| RunTestSequence(CheckViewProperty( |
| kTabbedPaneId, &TabbedPane::GetSelectedTabIndex, testing::Eq(1U)))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, WaitForViewProperty_AlreadyTrue) { |
| RunTestSequence(WaitForViewProperty(kButton1Id, View, Enabled, true)); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, WaitForViewProperty_BecomesTrue) { |
| button1_->SetEnabled(false); |
| DoPost(base::BindLambdaForTesting([this]() { button1_->SetEnabled(true); })); |
| RunTestSequence(WaitForViewProperty(kButton1Id, View, Enabled, true)); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, PollView) { |
| using Observer = PollingViewObserver<std::u16string, LabelButton>; |
| DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(Observer, kButtonTextState); |
| DoPost(base::BindLambdaForTesting( |
| [this]() { button1_->SetText(kButton2Caption); })); |
| RunTestSequence(PollView(kButtonTextState, kButton1Id, |
| [](const LabelButton* b) { |
| return std::u16string(b->GetText()); |
| }), |
| WaitForState(kButtonTextState, kButton2Caption)); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, PollViewProperty) { |
| using Observer = PollingViewPropertyObserver<std::u16string, LabelButton>; |
| DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(Observer, kButtonTextState); |
| DoPost(base::BindLambdaForTesting( |
| [this]() { button1_->SetText(kButton2Caption); })); |
| RunTestSequence( |
| PollViewProperty(kButtonTextState, kButton1Id, &LabelButton::GetText), |
| WaitForState(kButtonTextState, kButton2Caption)); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, WaitForViewPropertyFails) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| private_test_impl().set_aborted_callback_for_testing(aborted.Get()); |
| button1_->SetEnabled(false); |
| DoPost(base::BindLambdaForTesting([this]() { button1_->SetVisible(false); })); |
| EXPECT_CALL_IN_SCOPE( |
| aborted, Run, |
| RunTestSequence(WaitForViewProperty(kButton1Id, View, Enabled, true))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, WaitForViewPropertyInParallel) { |
| button1_->SetEnabled(false); |
| tabs_->SetEnabled(false); |
| RunTestSequence(InParallel( |
| RunSubsequence( |
| // These have to be inside the subsequences because there's an |
| // implicit flush before a subsequence starts; if we queued them |
| // all up ahead of time we wouldn't accurately be testing the |
| // multiple state change reactions (they'd already be true). |
| Post(base::BindLambdaForTesting( |
| [this]() { tabs_->SetEnabled(true); })), |
| WaitForViewProperty(kButton1Id, View, Enabled, true), |
| Post(base::BindLambdaForTesting([this]() { button1_->SetID(998); })), |
| WaitForViewProperty(kButton1Id, View, ID, 998)), |
| RunSubsequence( |
| Post(base::BindLambdaForTesting( |
| [this]() { button1_->SetEnabled(true); })), |
| WaitForViewProperty(kTabbedPaneId, View, Enabled, true), |
| Post(base::BindLambdaForTesting([this]() { tabs_->SetID(999); })), |
| WaitForViewProperty(kTabbedPaneId, View, ID, 999)))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameViewAbsoluteValue) { |
| RunTestSequence( |
| NameView(kViewName, button2_.get()), |
| WithElement(kViewName, |
| base::BindLambdaForTesting([&](ui::TrackedElement* el) { |
| EXPECT_EQ(button2_.get(), AsView<LabelButton>(el)); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameViewAbsoluteDeferred) { |
| View* view = nullptr; |
| RunTestSequence( |
| Do(base::BindLambdaForTesting([&]() { view = button2_.get(); })), |
| NameView(kViewName, std::ref(view)), |
| WithElement(kViewName, |
| base::BindLambdaForTesting([&](ui::TrackedElement* el) { |
| EXPECT_EQ(view, AsView(el)); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameViewAbsoluteCallback) { |
| RunTestSequence( |
| NameView(kViewName, base::BindLambdaForTesting( |
| [&]() -> View* { return button2_.get(); })), |
| WithElement(kViewName, |
| base::BindLambdaForTesting([&](ui::TrackedElement* el) { |
| EXPECT_EQ(button2_.get(), AsView<LabelButton>(el)); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameChildViewByIndex) { |
| RunTestSequence( |
| NameChildView(kButtonsId, kViewName, 1U), |
| WithElement(kViewName, |
| base::BindLambdaForTesting([&](ui::TrackedElement* el) { |
| auto* const button = AsView<LabelButton>(el); |
| EXPECT_EQ(button2_.get(), button); |
| EXPECT_EQ(1U, button->parent()->GetIndexOf(button)); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameChildViewByFilter) { |
| EXPECT_CALL_IN_SCOPE( |
| button2_callback_, Run, |
| RunTestSequence( |
| NameChildView( |
| kButtonsId, kViewName, base::BindRepeating([](const View* view) { |
| const auto* const button = AsViewClass<LabelButton>(view); |
| return button && button->GetText() == kButton2Caption; |
| })), |
| PressButton(kViewName, InputType::kKeyboard))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameDescendantView) { |
| EXPECT_CALL_IN_SCOPE( |
| button1_callback_, Run, |
| RunTestSequence(NameDescendantView( |
| kContentsId, kViewName, |
| base::BindRepeating([&](const View* view) { |
| return view->GetProperty(kElementIdentifierKey) == |
| kButton1Id; |
| })), |
| PressButton(kViewName, InputType::kMouse))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameViewRelative) { |
| RunTestSequence( |
| SelectTab(kTabbedPaneId, 1U, InputType::kTouch), |
| NameViewRelative(kTabbedPaneId, kViewName, |
| base::BindRepeating([&](TabbedPane* tabs) { |
| return tabs->GetTabContentsForTesting(1); |
| })), |
| WithElement(kViewName, |
| base::BindLambdaForTesting([&](ui::TrackedElement* el) { |
| EXPECT_EQ(kTab2Contents, AsView<Label>(el)->GetText()); |
| }))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameChildViewFails) { |
| UNCALLED_MOCK_CALLBACK(ui::InteractionSequence::AbortedCallback, aborted); |
| private_test_impl().set_aborted_callback_for_testing(aborted.Get()); |
| EXPECT_CALL_IN_SCOPE( |
| aborted, Run, |
| RunTestSequence( |
| NameChildView( |
| kButtonsId, kViewName, base::BindRepeating([](const View* view) { |
| const auto* const button = AsViewClass<LabelButton>(view); |
| return button && button->GetText() == |
| u"This is not a valid button caption."; |
| })), |
| PressButton(kViewName, InputType::kKeyboard))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameChildViewByTypeAndIndex) { |
| EXPECT_CALLS_IN_SCOPE_2( |
| button1_callback_, Run, button2_callback_, Run, |
| RunTestSequence( |
| NameChildViewByType<views::LabelButton>(kButtonsId, kViewName), |
| NameChildViewByType<views::LabelButton>(kButtonsId, kViewName2, 1), |
| PressButton(kViewName), PressButton(kViewName2))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, NameDescendantViewByTypeAndIndex) { |
| RunTestSequence( |
| NameDescendantViewByType<views::TabbedPaneTab>(kContentsId, kViewName), |
| NameDescendantViewByType<views::TabbedPaneTab>(kContentsId, kViewName2, |
| 2), |
| CheckViewProperty(kViewName, &views::TabbedPaneTab::GetTitleText, |
| kTab1Title), |
| CheckViewProperty(kViewName2, &views::TabbedPaneTab::GetTitleText, |
| kTab3Title)); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewTrue) { |
| UNCALLED_MOCK_CALLBACK(base::OnceCallback<bool(const LabelButton*)>, |
| condition); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(condition, Run(button1_.get())).WillOnce(testing::Return(true)); |
| EXPECT_CALL(step1, Run); |
| RunTestSequence(IfView(kButton1Id, condition.Get(), Then(Do(step1.Get())), |
| Else(Do(step2.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewFalse) { |
| UNCALLED_MOCK_CALLBACK(base::OnceCallback<bool(const LabelButton*)>, |
| condition); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(condition, Run(button1_.get())).WillOnce(testing::Return(false)); |
| EXPECT_CALL(step2, Run); |
| RunTestSequence(IfView(kButton1Id, condition.Get(), Then(Do(step1.Get())), |
| Else(Do(step2.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewMatchesTrue) { |
| UNCALLED_MOCK_CALLBACK(base::OnceCallback<int(const LabelButton*)>, |
| condition); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(condition, Run(button1_.get())).WillOnce(testing::Return(1)); |
| EXPECT_CALL(step1, Run); |
| RunTestSequence(IfViewMatches(kButton1Id, condition.Get(), 1, |
| Then(Do(step1.Get())), Else(Do(step2.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewMatchesFalse) { |
| UNCALLED_MOCK_CALLBACK(base::OnceCallback<int(const LabelButton*)>, |
| condition); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(condition, Run(button1_.get())).WillOnce(testing::Return(2)); |
| EXPECT_CALL(step2, Run); |
| RunTestSequence(IfViewMatches(kButton1Id, condition.Get(), 1, |
| Then(Do(step1.Get())), Else(Do(step2.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewPropertyMatchesTrue) { |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(step1, Run); |
| RunTestSequence(IfViewPropertyMatches( |
| kButton1Id, &LabelButton::GetText, std::u16string(kButton1Caption), |
| Then(Do(step1.Get())), Else(Do(step2.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, IfViewPropertyMatchesFalse) { |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step1); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, step2); |
| |
| EXPECT_CALL(step2, Run); |
| RunTestSequence(IfViewPropertyMatches( |
| kButton1Id, &LabelButton::GetText, testing::Ne(kButton1Caption), |
| Then(Do(step1.Get())), Else(Do(step2.Get())))); |
| } |
| |
| // Test that elements named in the main test sequence are available in |
| // subsequences. |
| TEST_F(InteractiveViewsTestTest, InParallelNamedView) { |
| auto is_view = []() { |
| return base::BindOnce([](View* actual) { return actual; }); |
| }; |
| |
| RunTestSequence( |
| // Name two views. Each will be referenced in a subsequence. |
| NameView(kViewName, button1_.get()), NameView(kViewName2, button2_.get()), |
| // Run subsequences, each of which references a different named view from |
| // the outer sequence. Both should succeed. |
| InParallel(RunSubsequence(CheckView(kViewName, is_view(), button1_)), |
| RunSubsequence(CheckView(kViewName2, is_view(), button2_)))); |
| } |
| |
| // Test that various automatic binding methods work with verbs and conditions. |
| TEST_F(InteractiveViewsTestTest, BindingMethods) { |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, correct); |
| UNCALLED_MOCK_CALLBACK(base::OnceClosure, incorrect); |
| |
| auto get_second_tab = [](TabbedPane* tabs) { return tabs->GetTabAt(1U); }; |
| |
| EXPECT_CALL(correct, Run).Times(2); |
| RunTestSequence( |
| SelectTab(kTabbedPaneId, 1U), |
| NameViewRelative(kTabbedPaneId, kViewName, get_second_tab), |
| WithView(kViewName, [](TabbedPaneTab* tab) { EXPECT_NE(nullptr, tab); }), |
| IfView( |
| kViewName, [](const TabbedPaneTab* tab) { return tab != nullptr; }, |
| Then(Do(correct.Get())), Else(Do(incorrect.Get()))), |
| IfViewMatches( |
| kViewName, |
| [this](const TabbedPaneTab* tab) { return tabs_->GetIndexOf(tab); }, |
| 0U, Then(Do(incorrect.Get())), Else(Do(correct.Get())))); |
| } |
| |
| TEST_F(InteractiveViewsTestTest, ScrollIntoView) { |
| const auto visible = [this](View* view) { |
| const gfx::Rect bounds = view->GetBoundsInScreen(); |
| const gfx::Rect scroll_bounds = scroll_->GetBoundsInScreen(); |
| return bounds.Intersects(scroll_bounds); |
| }; |
| |
| RunTestSequence(CheckView(kScrollChild1Id, visible, true), |
| CheckView(kScrollChild2Id, visible, false), |
| ScrollIntoView(kScrollChild2Id), |
| CheckView(kScrollChild2Id, visible, true), |
| ScrollIntoView(kScrollChild1Id), |
| CheckView(kScrollChild1Id, visible, true)); |
| } |
| |
| } // namespace views::test |
| |
| // Verifies that WaitForViewProperty() compiles outside of the views namespace |
| // (this was a problem previously). |
| class InteractiveViewsTestCompileTest |
| : public views::test::InteractiveViewsTestTest { |
| public: |
| InteractiveViewsTestCompileTest() = default; |
| ~InteractiveViewsTestCompileTest() override = default; |
| |
| void WaitForViewPropertyCompileOutsideViews() { |
| (void)WaitForViewProperty(views::test::kButton1Id, views::View, Enabled, |
| true); |
| } |
| }; |