blob: 52f11285960653ec34a1b95085c8e9cd036d3914 [file]
// Copyright 2025 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/webui/tracked_element/tracked_element_handler.h"
#include <memory>
#include "base/test/bind.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_content_client_initializer.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_web_ui.h"
#include "content/public/test/web_contents_tester.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/element_events.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/views/controls/webview/webview.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget.h"
#include "ui/webui/resources/js/tracked_element/tracked_element.mojom.h"
#include "ui/webui/tracked_element/tracked_element_web_ui.h"
namespace ui {
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestElementIdentifier1);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestElementIdentifier2);
constexpr gfx::RectF kElementBounds{10, 20, 30, 40};
constexpr gfx::RectF kElementBounds2{15, 25, 35, 45};
} // namespace
class TrackedElementHandlerTest : public views::test::WidgetTest {
public:
TrackedElementHandlerTest()
: views::test::WidgetTest(std::unique_ptr<base::test::TaskEnvironment>(
std::make_unique<content::BrowserTaskEnvironment>())) {}
~TrackedElementHandlerTest() override = default;
void SetUp() override {
content::SetBrowserClientForTesting(&test_browser_client_);
rvh_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();
views::test::WidgetTest::SetUp();
browser_context_ = std::make_unique<content::TestBrowserContext>();
web_contents_ = content::WebContentsTester::CreateTestWebContents(
browser_context_.get(), nullptr);
mojo::PendingRemote<tracked_element::mojom::TrackedElementHandler> remote;
handler_ = std::make_unique<TrackedElementHandler>(
web_contents_.get(), remote.InitWithNewPipeAndPassReceiver(),
// When there is a consistent way of assigning contexts, use that.
ui::ElementContext::CreateFakeContextForTesting(web_contents_.get()),
std::vector<ui::ElementIdentifier>{kTestElementIdentifier1,
kTestElementIdentifier2});
tracked_element_handler_remote_.Bind(std::move(remote));
}
void TearDown() override {
tracked_element_handler_remote_.reset();
handler_.reset();
web_contents_.reset();
browser_context_.reset();
rvh_enabler_.reset();
views::test::WidgetTest::TearDown();
}
protected:
tracked_element::mojom::TrackedElementHandler* handler_remote() {
return tracked_element_handler_remote_.get();
}
TrackedElementHandler* handler() { return handler_.get(); }
content::ContentBrowserClient test_browser_client_;
std::unique_ptr<content::RenderViewHostTestEnabler> rvh_enabler_;
std::unique_ptr<content::BrowserContext> browser_context_;
std::unique_ptr<content::WebContents> web_contents_;
std::unique_ptr<content::TestWebUI> test_web_ui_;
std::unique_ptr<TrackedElementHandler> handler_;
mojo::Remote<tracked_element::mojom::TrackedElementHandler>
tracked_element_handler_remote_;
};
TEST_F(TrackedElementHandlerTest, StartsWithNoElement) {
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
}
TEST_F(TrackedElementHandlerTest, ElementCreatedOnEvent) {
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const element =
ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1);
EXPECT_TRUE(element);
EXPECT_TRUE(element->IsA<TrackedElementWebUI>());
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
// Verify that we don't leave elements dangling if the handler is destroyed.
handler_.reset();
tracked_element_handler_remote_.FlushForTesting();
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
}
TEST_F(TrackedElementHandlerTest, ElementHiddenOnEvent) {
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), false, gfx::RectF());
tracked_element_handler_remote_.FlushForTesting();
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
}
TEST_F(TrackedElementHandlerTest, ElementActivatedOnEvent) {
UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, activated);
const std::string name = kTestElementIdentifier1.GetName();
handler_remote()->TrackedElementVisibilityChanged(name, true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const tracker = ui::ElementTracker::GetElementTracker();
auto* const element =
tracker->GetElementInAnyContext(kTestElementIdentifier1);
ASSERT_TRUE(element);
auto subscription =
ui::ElementTracker::GetElementTracker()->AddElementActivatedCallback(
element->identifier(), element->context(), activated.Get());
EXPECT_CALL_IN_SCOPE(activated, Run(element), {
handler_remote()->TrackedElementActivated(name);
tracked_element_handler_remote_.FlushForTesting();
});
}
TEST_F(TrackedElementHandlerTest, ElementCustomEventOnEvent) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kCustomEvent);
kCustomEvent.GetName(); // Register it.
const std::string event_name = kCustomEvent.GetName();
const std::string element_name = kTestElementIdentifier1.GetName();
UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, custom_event);
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const tracker = ui::ElementTracker::GetElementTracker();
auto* const element =
tracker->GetElementInAnyContext(kTestElementIdentifier1);
ASSERT_TRUE(element);
auto subscription =
ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
kCustomEvent, element->context(), custom_event.Get());
EXPECT_CALL_IN_SCOPE(custom_event, Run(element), {
handler_remote()->TrackedElementCustomEvent(element_name, event_name);
tracked_element_handler_remote_.FlushForTesting();
});
}
TEST_F(TrackedElementHandlerTest,
ElementBoundsChangedEventFiredOnBoundsChange) {
UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, bounds_changed);
const std::string element_name = kTestElementIdentifier1.GetName();
// Make element visible with initial bounds.
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const tracker = ui::ElementTracker::GetElementTracker();
auto* const element =
tracker->GetElementInAnyContext(kTestElementIdentifier1);
ASSERT_TRUE(element);
// Subscribe to bounds changed event.
auto subscription =
ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
kElementBoundsChangedEvent, element->context(), bounds_changed.Get());
// Change bounds - should trigger the event.
EXPECT_CALL_IN_SCOPE(bounds_changed, Run(element), {
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds2);
tracked_element_handler_remote_.FlushForTesting();
});
}
TEST_F(TrackedElementHandlerTest,
ElementBoundsChangedEventNotFiredWhenBoundsUnchanged) {
UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, bounds_changed);
const std::string element_name = kTestElementIdentifier1.GetName();
// Make element visible with initial bounds.
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const tracker = ui::ElementTracker::GetElementTracker();
auto* const element =
tracker->GetElementInAnyContext(kTestElementIdentifier1);
ASSERT_TRUE(element);
// Subscribe to bounds changed event.
auto subscription =
ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
kElementBoundsChangedEvent, element->context(), bounds_changed.Get());
// Send same bounds - should NOT trigger the event.
EXPECT_CALL(bounds_changed, Run).Times(0);
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
}
TEST_F(TrackedElementHandlerTest, ElementBoundsChangedEventNotFiredWhenHidden) {
UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, bounds_changed);
const std::string element_name = kTestElementIdentifier1.GetName();
// Make element visible with initial bounds.
handler_remote()->TrackedElementVisibilityChanged(element_name, true,
kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const tracker = ui::ElementTracker::GetElementTracker();
auto* const element =
tracker->GetElementInAnyContext(kTestElementIdentifier1);
ASSERT_TRUE(element);
// Subscribe to bounds changed event.
auto subscription =
ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
kElementBoundsChangedEvent, element->context(), bounds_changed.Get());
// Hide element - should NOT trigger bounds changed event.
EXPECT_CALL(bounds_changed, Run).Times(0);
handler_remote()->TrackedElementVisibilityChanged(element_name, false,
gfx::RectF());
tracked_element_handler_remote_.FlushForTesting();
}
TEST_F(TrackedElementHandlerTest, MultipleIdentifiers) {
// Show two elements.
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier2.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
// Hide one element.
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), false, gfx::RectF());
tracked_element_handler_remote_.FlushForTesting();
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
// Hide the other element.
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier2.GetName(), false, gfx::RectF());
tracked_element_handler_remote_.FlushForTesting();
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
// Re-show an element.
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1));
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier2));
}
TEST_F(TrackedElementHandlerTest, DestroyHandlerCleansUpElement) {
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const element =
ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1);
ASSERT_TRUE(element);
const ui::ElementContext context = element->context();
EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->IsElementVisible(
kTestElementIdentifier1, context));
handler_.reset();
tracked_element_handler_remote_.FlushForTesting();
EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->IsElementVisible(
kTestElementIdentifier1, context));
}
TEST_F(TrackedElementHandlerTest, GetNativeView) {
handler_remote()->TrackedElementVisibilityChanged(
kTestElementIdentifier1.GetName(), true, kElementBounds);
tracked_element_handler_remote_.FlushForTesting();
auto* const element =
ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
kTestElementIdentifier1);
ASSERT_TRUE(element);
EXPECT_TRUE(element->IsA<TrackedElementWebUI>());
auto widget = std::make_unique<views::Widget>();
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::CLIENT_OWNS_WIDGET;
widget->Init(std::move(params));
widget->Show();
auto* webview = widget->SetClientContentsView(
std::make_unique<views::WebView>(browser_context_.get()));
webview->SetWebContents(web_contents_.get());
// The element should return the native view of the widget.
EXPECT_EQ(widget->GetNativeView(), element->GetNativeView());
webview->SetWebContents(nullptr);
widget->CloseNow();
}
// TODO(crbug.com/40243115): add tests for element screen bounds. This requires
// an update to the TestWebContents API to fake
// WebContents::GetContainerBounds().
} // namespace ui