blob: 9c66b0a275963692941203f06ff3d82af8644db3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/frame/browser_root_view.h"
#include <memory>
#include <vector>
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/defaults.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/navigation_handle_observer.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/point_f.h"
class BrowserRootViewBrowserTest : public InProcessBrowserTest {
public:
BrowserRootViewBrowserTest() = default;
BrowserRootViewBrowserTest(const BrowserRootViewBrowserTest&) = delete;
BrowserRootViewBrowserTest& operator=(const BrowserRootViewBrowserTest&) =
delete;
BrowserRootView* browser_root_view() {
BrowserView* browser_view = static_cast<BrowserView*>(browser()->window());
return static_cast<BrowserRootView*>(
browser_view->GetWidget()->GetRootView());
}
void PerformMouseWheelOnTabStrip(const gfx::Vector2d& offset) {
TabStrip* tabstrip =
BrowserView::GetBrowserViewForBrowser(browser())->tabstrip();
const gfx::Point tabstrip_center = tabstrip->GetLocalBounds().CenterPoint();
const gfx::Point location = views::View::ConvertPointToTarget(
tabstrip, browser_root_view(), tabstrip_center);
const gfx::Point root_location =
views::View::ConvertPointToScreen(tabstrip, tabstrip_center);
ui::MouseWheelEvent wheel_event(offset, location, root_location,
ui::EventTimeForNow(), /*flags=*/0,
/*changed_button_flags=*/0);
browser_root_view()->OnMouseWheel(wheel_event);
}
void WaitForDragStart(const ui::DropTargetEvent& event) {
base::RunLoop run_loop;
BrowserRootView* const root_view = browser_root_view();
root_view->SetOnFilteringCompleteClosureForTesting(run_loop.QuitClosure());
root_view->OnDragEntered(event);
run_loop.Run();
}
void StartAndFinishDrag(const ui::OSExchangeData& data,
ui::mojom::DragOperation& out_drag_op) {
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
WaitForDragStart(event);
// At this point, the drag information will have been set, and a background
// task will have been posted to process the dragged URLs
// (`GetURLMimeTypes()` -> `FilterURLs()`). Ensure that all background
// processing is complete before checking the drag operation or invoking the
// drag callback.
BrowserRootView* const root_view = browser_root_view();
EXPECT_NE(ui::DragDropTypes::DRAG_NONE, root_view->OnDragUpdated(event));
auto drop_cb = root_view->GetDropCallback(event);
std::move(drop_cb).Run(event, out_drag_op,
/*drag_image_layer_owner=*/nullptr);
}
};
// Clear drop info after performing drop. http://crbug.com/838791
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, ClearDropInfo) {
ui::OSExchangeData data;
data.SetURL(GURL("http://www.chromium.org/"), std::u16string());
auto* tab_strip_model = browser()->tab_strip_model();
EXPECT_EQ(tab_strip_model->count(), 1);
ui::mojom::DragOperation drag_op = ui::mojom::DragOperation::kNone;
StartAndFinishDrag(data, drag_op);
EXPECT_EQ(drag_op, ui::mojom::DragOperation::kCopy);
EXPECT_EQ(tab_strip_model->count(), 2);
EXPECT_FALSE(browser_root_view()->drop_info_);
}
// Make sure plain string is droppable. http://crbug.com/838794
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, PlainString) {
ui::OSExchangeData data;
data.SetString(u"Plain string");
ui::mojom::DragOperation drag_op = ui::mojom::DragOperation::kNone;
StartAndFinishDrag(data, drag_op);
EXPECT_NE(ui::mojom::DragOperation::kNone, drag_op);
}
// Clear drop target when the widget is being destroyed.
// http://crbug.com/1001942
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, ClearDropTarget) {
ui::OSExchangeData data;
data.SetURL(GURL("http://www.chromium.org/"), std::u16string());
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
browser_root_view()->OnDragUpdated(event);
// Calling this will cause segmentation fault if |root_view| doesn't clear
// the target.
CloseBrowserSynchronously(browser());
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, OnDragEnteredNoTabs) {
auto* tab_strip_model = browser()->tab_strip_model();
EXPECT_EQ(tab_strip_model->count(), 1);
EXPECT_EQ(tab_strip_model->active_index(), 0);
tab_strip_model->CloseAllTabs();
EXPECT_EQ(tab_strip_model->count(), 0);
EXPECT_EQ(tab_strip_model->active_index(), -1);
ui::OSExchangeData data;
data.SetURL(GURL("file:///test.txt"), std::u16string());
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
// Ensure OnDragEntered() doesn't crash when there are no active tabs.
browser_root_view()->OnDragEntered(event);
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, WheelTabChange) {
if (!browser_defaults::kScrollEventChangesTab) {
GTEST_SKIP() << "Test does not apply to this platform.";
}
TabStripModel* model = browser()->tab_strip_model();
while (model->count() < 2) {
ASSERT_TRUE(
AddTabAtIndex(0, GURL(url::kAboutBlankURL), ui::PAGE_TRANSITION_LINK));
}
model->ActivateTabAt(1);
ASSERT_EQ(1, model->active_index());
const gfx::Vector2d kWheelDown(0, -ui::MouseWheelEvent::kWheelDelta);
const gfx::Vector2d kWheelUp(0, ui::MouseWheelEvent::kWheelDelta);
// At the end of the tab strip, scrolling to the next tab does not loop around
// to the beginning.
PerformMouseWheelOnTabStrip(kWheelDown);
EXPECT_EQ(1, model->active_index());
PerformMouseWheelOnTabStrip(kWheelUp);
EXPECT_EQ(0, model->active_index());
// At the beginning of the tab strip, scrolling to the previous tab does not
// loop around to the end.
PerformMouseWheelOnTabStrip(kWheelUp);
EXPECT_EQ(0, model->active_index());
PerformMouseWheelOnTabStrip(kWheelDown);
EXPECT_EQ(1, model->active_index());
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest,
WheelTabChangeWithCollapsedTabGroups) {
if (!browser_defaults::kScrollEventChangesTab) {
GTEST_SKIP() << "Test does not apply to this platform.";
}
TabStripModel* model = browser()->tab_strip_model();
TabStrip* tabstrip =
BrowserView::GetBrowserViewForBrowser(browser())->tabstrip();
ASSERT_TRUE(model->SupportsTabGroups());
// Create 5 tabs, with the leftmost, center, and rightmost in collapsed tab
// groups.
while (model->count() < 5) {
ASSERT_TRUE(
AddTabAtIndex(0, GURL(url::kAboutBlankURL), ui::PAGE_TRANSITION_LINK));
}
model->ActivateTabAt(1);
tab_groups::TabGroupId left_group = model->AddToNewGroup({0});
tab_groups::TabGroupId center_group = model->AddToNewGroup({2});
tab_groups::TabGroupId right_group = model->AddToNewGroup({4});
tabstrip->ToggleTabGroupCollapsedState(left_group);
tabstrip->ToggleTabGroupCollapsedState(center_group);
tabstrip->ToggleTabGroupCollapsedState(right_group);
ASSERT_EQ(1, model->active_index());
ASSERT_TRUE(model->IsTabCollapsed(0));
ASSERT_FALSE(model->IsTabCollapsed(1));
ASSERT_TRUE(model->IsTabCollapsed(2));
ASSERT_FALSE(model->IsTabCollapsed(3));
ASSERT_TRUE(model->IsTabCollapsed(4));
const gfx::Vector2d kWheelDown(0, -ui::MouseWheelEvent::kWheelDelta);
const gfx::Vector2d kWheelUp(0, ui::MouseWheelEvent::kWheelDelta);
// Scrolling to the previous tab from index 1 should do nothing, since the
// only previous tab is collapsed.
PerformMouseWheelOnTabStrip(kWheelUp);
EXPECT_EQ(1, model->active_index());
// Scrolling to the next tab from index 1 should skip to index 3, since index
// 2 in between them is collapsed.
PerformMouseWheelOnTabStrip(kWheelDown);
EXPECT_EQ(3, model->active_index());
// Scrolling to the next tab from index 3 should do nothing, since the only
// next tab is collapsed.
PerformMouseWheelOnTabStrip(kWheelDown);
EXPECT_EQ(3, model->active_index());
}
// TODO(crbug.com/386194202): Flaky on Linux.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_DropOrderingCorrect DISABLED_DropOrderingCorrect
#else
#define MAYBE_DropOrderingCorrect DropOrderingCorrect
#endif
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, MAYBE_DropOrderingCorrect) {
TabStripModel* model = browser()->tab_strip_model();
// HELPER FUNCTION: Verify that the tabs in the current browser window match
// the expected list of tabs.
auto assert_tab_order = [model](std::vector<std::string_view> tab_urls,
std::optional<int> active_tab) {
ASSERT_EQ(static_cast<size_t>(model->count()), tab_urls.size());
for (size_t i = 0; i < tab_urls.size(); ++i) {
content::WebContents* contents = model->GetWebContentsAt(i);
EXPECT_TRUE(content::WaitForLoadStop(contents));
ASSERT_EQ(contents->GetURL(), GURL(tab_urls[i]));
}
if (active_tab.has_value()) {
EXPECT_EQ(model->active_index(), active_tab.value());
}
};
// HELPER FUNCTION: Reset to three tabs:
// [about:blank?1][about:blank?2][about:blank?3]
auto reset_tabs = [this, model, assert_tab_order]() {
ASSERT_TRUE(
AddTabAtIndex(0, GURL("about:blank?1"), ui::PAGE_TRANSITION_LINK));
ASSERT_TRUE(
AddTabAtIndex(1, GURL("about:blank?2"), ui::PAGE_TRANSITION_LINK));
ASSERT_TRUE(
AddTabAtIndex(2, GURL("about:blank?3"), ui::PAGE_TRANSITION_LINK));
while (model->count() > 3) {
model->DetachAndDeleteWebContentsAt(3);
}
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?3"},
std::nullopt);
};
// HELPER FUNCTION: Generate a DropInfo pointer.
using BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex;
using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
auto make_drop_info =
[](std::vector<std::string_view> url_strings, int index,
BrowserRootView::DropIndex::RelativeToIndex relative_to_index) {
std::vector<GURL> urls;
for (const auto& url_string : url_strings) {
urls.emplace_back(url_string);
}
std::unique_ptr<BrowserRootView::DropInfo> drop_info;
drop_info = std::make_unique<BrowserRootView::DropInfo>();
drop_info->urls = std::move(urls);
drop_info->index.emplace();
drop_info->index->index = index;
drop_info->index->relative_to_index = relative_to_index;
return drop_info;
};
// A bunch of variables that are needed for the calls to NavigateToDroppedUrls
// but that aren't tested here.
OSExchangeData os_exchange_data;
ui::DropTargetEvent event(os_exchange_data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DragOperation::DRAG_COPY);
ui::mojom::DragOperation drag_operation;
std::unique_ptr<BrowserRootView::DropInfo> drop_info;
// Finally, the actual tests!
{
SCOPED_TRACE("Insert single URL on left");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 0, kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order(
{"about:blank?A", "about:blank?1", "about:blank?2", "about:blank?3"},
0);
}
{
SCOPED_TRACE("Insert single URL between two tabs");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 1, kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order(
{"about:blank?1", "about:blank?A", "about:blank?2", "about:blank?3"},
1);
}
{
SCOPED_TRACE("Insert single URL on right");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 3, kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order(
{"about:blank?1", "about:blank?2", "about:blank?3", "about:blank?A"},
3);
}
{
SCOPED_TRACE("Insert multiple URLs on left");
reset_tabs();
drop_info =
make_drop_info({"about:blank?A", "about:blank?B", "about:blank?C"}, 0,
kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?A", "about:blank?B", "about:blank?C",
"about:blank?1", "about:blank?2", "about:blank?3"},
0);
}
{
SCOPED_TRACE("Insert multiple URLs between two tabs");
reset_tabs();
drop_info =
make_drop_info({"about:blank?A", "about:blank?B", "about:blank?C"}, 1,
kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?A", "about:blank?B",
"about:blank?C", "about:blank?2", "about:blank?3"},
1);
}
{
SCOPED_TRACE("Insert multiple URLs on right");
reset_tabs();
drop_info =
make_drop_info({"about:blank?A", "about:blank?B", "about:blank?C"}, 3,
kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?3",
"about:blank?A", "about:blank?B", "about:blank?C"},
3);
}
{
SCOPED_TRACE("Replace single URL on left");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 0, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?A", "about:blank?2", "about:blank?3"}, 0);
}
{
SCOPED_TRACE("Replace single URL between two tabs");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 1, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?A", "about:blank?3"}, 1);
}
{
SCOPED_TRACE("Replace single URL on right");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 2, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?A"}, 2);
}
{
SCOPED_TRACE("Replace multiple URLs on left");
reset_tabs();
drop_info = make_drop_info(
{"about:blank?A", "about:blank?B", "about:blank?C"}, 0, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?A", "about:blank?B", "about:blank?C",
"about:blank?2", "about:blank?3"},
0);
}
{
SCOPED_TRACE("Replace multiple URLs between two tabs");
reset_tabs();
drop_info = make_drop_info(
{"about:blank?A", "about:blank?B", "about:blank?C"}, 1, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?A", "about:blank?B",
"about:blank?C", "about:blank?3"},
1);
}
{
SCOPED_TRACE("Replace multiple URLs on right");
reset_tabs();
drop_info = make_drop_info(
{"about:blank?A", "about:blank?B", "about:blank?C"}, 2, kReplaceIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?A",
"about:blank?B", "about:blank?C"},
2);
}
{
SCOPED_TRACE("Error case: no index");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 0, kInsertBeforeIndex);
drop_info->index.reset();
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?3"},
std::nullopt);
}
{
SCOPED_TRACE("Error case: index too large");
reset_tabs();
drop_info = make_drop_info({"about:blank?A"}, 10, kInsertBeforeIndex);
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
assert_tab_order({"about:blank?1", "about:blank?2", "about:blank?3"},
std::nullopt);
}
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest,
InitiatorOriginForDroppedLink) {
TabStripModel* model = browser()->tab_strip_model();
ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
ui::mojom::DragOperation drag_operation;
// Set drop event data with initiator origin.
ui::OSExchangeData data;
GURL target_url("about:blank?1");
url::Origin initiator_origin =
url::Origin::Create(GURL("https://www.initiator.com/"));
data.SetURL(target_url, std::u16string());
data.MarkRendererTaintedFromOrigin(initiator_origin);
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
std::unique_ptr<BrowserRootView::DropInfo> drop_info =
std::make_unique<BrowserRootView::DropInfo>();
drop_info->urls.push_back(target_url);
drop_info->index.emplace();
drop_info->index->index = 0;
drop_info->index->relative_to_index = kReplaceIndex;
// Add observer to web contents to track the navigation.
content::WebContents* contents = model->GetActiveWebContents();
content::NavigationHandleObserver observer(contents, target_url);
// Call NavigateToDroppedUrls and verify the initiator origin.
browser_root_view()->NavigateToDroppedUrls(std::move(drop_info), event,
drag_operation, nullptr);
content::WaitForLoadStop(contents);
EXPECT_EQ(target_url, contents->GetLastCommittedURL());
EXPECT_TRUE(observer.last_initiator_origin().has_value());
EXPECT_EQ(initiator_origin, observer.last_initiator_origin().value());
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, NavigateToUrlFromText) {
ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
ui::OSExchangeData data;
data.SetString(u"chrome://settings/");
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
WaitForDragStart(event);
EXPECT_EQ(ui::DragDropTypes::DRAG_COPY,
browser_root_view()->OnDragUpdated(event));
}
IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest,
DoesNotNavigateToUrlFromRendererTaintedText) {
ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
ui::OSExchangeData data;
data.SetString(u"chrome://settings/");
data.MarkRendererTaintedFromOrigin(url::Origin());
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
WaitForDragStart(event);
EXPECT_EQ(ui::DragDropTypes::DRAG_NONE,
browser_root_view()->OnDragUpdated(event));
}