Index: content/browser/web_contents/drag_and_drop_browsertest.cc |
diff --git a/content/browser/web_contents/drag_and_drop_browsertest.cc b/content/browser/web_contents/drag_and_drop_browsertest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4d6b7aaf3098b151104a06888620494fe40562e4 |
--- /dev/null |
+++ b/content/browser/web_contents/drag_and_drop_browsertest.cc |
@@ -0,0 +1,343 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include <memory> |
+#include <string> |
+ |
+#include "base/macros.h" |
+#include "base/strings/string_piece.h" |
+#include "base/strings/stringprintf.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "content/public/browser/render_frame_host.h" |
+#include "content/public/browser/web_contents.h" |
+#include "content/public/test/browser_test_utils.h" |
+#include "content/public/test/content_browser_test.h" |
+#include "content/public/test/content_browser_test_utils.h" |
+#include "content/public/test/test_utils.h" |
+#include "content/shell/browser/shell.h" |
+#include "net/base/escape.h" |
+#include "net/dns/mock_host_resolver.h" |
+#include "net/test/embedded_test_server/embedded_test_server.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+#include "ui/aura/client/drag_drop_delegate.h" |
+#include "ui/aura/client/screen_position_client.h" |
+#include "ui/aura/window.h" |
+#include "ui/base/dragdrop/drag_drop_types.h" |
+#include "ui/base/dragdrop/drop_target_event.h" |
+#include "ui/base/dragdrop/os_exchange_data.h" |
+#include "ui/gfx/geometry/point.h" |
+#include "ui/gfx/geometry/rect.h" |
+#include "url/gurl.h" |
+ |
+namespace content { |
+ |
+namespace { |
+ |
+// TODO(lukasza): Support testing on non-Aura platforms (i.e. Android + Mac?). |
+// |
+// Notes for the TODO above: |
+// |
+// - Why inject/simulate drag-and-drop events at the aura::Window* level. |
+// |
+// - It seems better to inject into UI libraries to cover code *inside* these |
+// libraries. This might complicate simulation a little bit (i.e. picking |
+// the right aura::Window and/or aura::client::DragDropDelegate to target), |
+// but otherwise important bits of code wouldn't get test coverage (i.e. |
+// directly injecting into RenderViewHost->DragTargetDragEnter seems wrong). |
+// |
+// - In theory, we could introduce WebContentsImpl::DragTargetDragEnter (to be |
+// used by all UI platforms - so reused by web_contents_view_android.cc, |
+// web_contents_view_aura.cc, web_drag_dest_mac.mm), but it feels wrong - UI |
+// libraries should already know which widget is the target of the event and |
+// so should be able to talk directly to the right widget (i.e. WebContents |
+// should not be responsible for mapping coordinates to a widget - this is |
+// the job of the UI library). |
+// |
+// - Unknowns: |
+// |
+// - Will this work for WebView and Plugin testing. |
+// |
+// - Will this work for simulating dragging from WebContents into outside of |
+// the browser (without leaving OS drag&drop machinery in a weird state). |
Łukasz Anforowicz
2016/11/01 23:43:41
We'll have to work through the items above, but I
|
+ |
+// Test helper for simulating drag and drop happening in WebContents. |
+class DragAndDropSimulator { |
+ public: |
+ DragAndDropSimulator(WebContents* web_contents) |
+ : web_contents_(web_contents) {} |
+ |
+ // Simulates notification that |text| was dragged from outside of the browser, |
+ // into the specified |location| inside |web_contents|. |
+ // |location| is relative to |web_contents|. |
+ // Returns true upon success. |
+ bool SimulateDragEnter(gfx::Point location, const std::string& text) { |
+ ui::OSExchangeData data; |
+ data.SetString(base::UTF8ToUTF16(text)); |
+ return SimulateDragEnter(location, data); |
+ } |
+ |
+ // Simulates dropping of the drag-and-dropped item. |
+ // SimulateDragEnter needs to be called first. |
+ // Returns true upon success. |
+ bool SimulateDrop(gfx::Point location) { |
+ if (!active_drag_event_) { |
+ ADD_FAILURE() << "Cannot drop a drag that hasn't started yet."; |
+ return false; |
+ } |
+ |
+ aura::client::DragDropDelegate* delegate = GetDragDropDelegate(); |
+ if (!delegate) |
+ return false; |
+ |
+ gfx::Point event_location; |
+ gfx::Point event_root_location; |
+ CalculateEventLocations(location, &event_location, &event_root_location); |
+ active_drag_event_->set_location(event_location); |
+ active_drag_event_->set_root_location(event_root_location); |
+ |
+ delegate->OnDragUpdated(*active_drag_event_); |
+ delegate->OnPerformDrop(*active_drag_event_); |
+ return true; |
+ } |
+ |
+ private: |
+ bool SimulateDragEnter(gfx::Point location, const ui::OSExchangeData& data) { |
+ if (active_drag_event_) { |
+ ADD_FAILURE() << "Cannot start a new drag when old one hasn't ended yet."; |
+ return false; |
+ } |
+ |
+ aura::client::DragDropDelegate* delegate = GetDragDropDelegate(); |
+ if (!delegate) |
+ return false; |
+ |
+ gfx::Point event_location; |
+ gfx::Point event_root_location; |
+ CalculateEventLocations(location, &event_location, &event_root_location); |
+ active_drag_event_.reset(new ui::DropTargetEvent( |
+ data, event_location, event_root_location, kDefaultSourceOperations)); |
+ |
+ delegate->OnDragEntered(*active_drag_event_); |
+ delegate->OnDragUpdated(*active_drag_event_); |
+ return true; |
+ } |
+ |
+ aura::client::DragDropDelegate* GetDragDropDelegate() { |
+ gfx::NativeView view = web_contents_->GetContentNativeView(); |
+ aura::client::DragDropDelegate* delegate = |
+ aura::client::GetDragDropDelegate(view); |
+ EXPECT_TRUE(delegate) << "Expecting WebContents to have DragDropDelegate"; |
+ return delegate; |
+ } |
+ |
+ void CalculateEventLocations(gfx::Point web_contents_relative_location, |
+ gfx::Point* out_event_location, |
+ gfx::Point* out_event_root_location) { |
+ gfx::NativeView view = web_contents_->GetNativeView(); |
+ |
+ *out_event_location = web_contents_relative_location; |
+ |
+ gfx::Point root_location = web_contents_relative_location; |
+ aura::Window::ConvertPointToTarget(view, view->GetRootWindow(), |
+ &root_location); |
+ *out_event_location = root_location; |
+ } |
+ |
+ // These are ui::DropTargetEvent::source_operations_ being sent when manually |
+ // trying out drag&drop of an image file from Nemo (Ubuntu's file explorer) |
+ // into a content_shell. |
+ static constexpr int kDefaultSourceOperations = ui::DragDropTypes::DRAG_MOVE | |
+ ui::DragDropTypes::DRAG_COPY | |
+ ui::DragDropTypes::DRAG_LINK; |
+ |
+ WebContents* web_contents_; |
+ std::unique_ptr<ui::DropTargetEvent> active_drag_event_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DragAndDropSimulator); |
+}; |
+ |
+// Helper for waiting for notifications from |
+// content/test/data/drag_and_drop/event_monitoring.js |
+class DOMDragEventWaiter : public DOMAutomationWaiter { |
ncarter (slow)
2016/11/01 19:50:52
I think this would be cleaner if this used composi
|
+ public: |
+ explicit DOMDragEventWaiter(const std::string& event_type_to_wait_for, |
+ const ToRenderFrameHost& target) |
+ : DOMAutomationWaiter( |
+ WebContents::FromRenderFrameHost(target.render_frame_host())), |
+ target_frame_name_(target.render_frame_host()->GetFrameName()), |
+ event_type_to_wait_for_(event_type_to_wait_for) {} |
+ |
+ // Waits until |target| calls reportDragEvent in |
+ // content/test/data/drag_and_drop/event_monitoring.js with event_type |
+ // property set to |event_type_to_wait_for|. (|target| and |
+ // |event_type_to_wait_for| are passed to the constructor). |
+ // |
+ // Returns the event details via |response|. See |
+ // content/test/data/drag_and_drop/event_monitoring.js for keys / properties |
+ // that |response| is expected to have. |
+ // |
+ // Returns true upon success. It is okay if |response| is null. |
+ bool WaitAndGetResponse(std::unique_ptr<base::DictionaryValue>* response) |
+ WARN_UNUSED_RESULT { |
+ std::unique_ptr<base::Value> untyped_response; |
+ if (!DOMAutomationWaiter::WaitAndGetResponse(&untyped_response)) |
+ return false; |
+ |
+ if (response) { |
+ *response = base::DictionaryValue::From(std::move(untyped_response)); |
+ if (!*response) |
+ return false; |
+ } |
+ |
+ return true; |
+ } |
+ |
+ protected: |
+ bool ShouldStopWaiting(const base::Value& value) override { |
ncarter (slow)
2016/11/01 19:50:52
Couldn't you just do this, with no changes to DOMM
|
+ const base::DictionaryValue* dictionary; |
+ if (!value.GetAsDictionary(&dictionary)) |
+ return false; |
+ |
+ std::string event_type; |
+ if (!dictionary->GetString("event_type", &event_type)) |
+ return false; |
+ |
+ std::string window_name; |
+ if (!dictionary->GetString("window_name", &window_name)) |
+ return false; |
+ |
+ return event_type == event_type_to_wait_for_ && |
+ window_name == target_frame_name_; |
+ } |
+ |
+ private: |
+ std::string target_frame_name_; |
+ std::string event_type_to_wait_for_; |
+}; |
+ |
+std::string GetStringOrEmpty(const base::DictionaryValue& dict, |
+ base::StringPiece path) { |
+ std::string result; |
+ if (dict.GetString(path, &result)) |
+ return result; |
+ return ""; |
+} |
+ |
+const char kTestPagePath[] = "/drag_and_drop/page.html"; |
+ |
+} // namespace |
+ |
+class DragAndDropBrowserTest : public ContentBrowserTest { |
+ public: |
+ DragAndDropBrowserTest(){}; |
+ |
+ protected: |
+ void SetUpOnMainThread() override { |
+ host_resolver()->AddRule("*", "127.0.0.1"); |
+ ASSERT_TRUE(embedded_test_server()->Start()); |
+ content::SetupCrossSiteRedirector(embedded_test_server()); |
+ drag_simulator_.reset(new DragAndDropSimulator(shell()->web_contents())); |
+ } |
+ |
+ // Navigates to content/test/data/drag_and_drop/page.html, with page origin |
+ // and frame contents being controlled by the parameters. |
+ bool NavigateToTestPage(const std::string& main_origin, |
+ const std::string& left_frame, |
+ const std::string& right_frame) { |
+ GURL base_url = embedded_test_server()->GetURL(main_origin, kTestPagePath); |
+ |
+ std::string left_arg; |
+ if (!left_frame.empty()) { |
+ left_arg = net::EscapeQueryParamValue( |
+ std::string("/cross-site/") + left_frame, true); |
+ } |
+ std::string right_arg; |
+ if (!right_frame.empty()) { |
+ right_arg = net::EscapeQueryParamValue( |
+ std::string("/cross-site/") + right_frame, true); |
+ } |
+ std::string query = base::StringPrintf("left=%s&right=%s", left_arg.c_str(), |
+ right_arg.c_str()); |
+ GURL::Replacements query_replacement; |
+ query_replacement.SetQueryStr(query); |
+ GURL target_url = base_url.ReplaceComponents(query_replacement); |
+ |
+ return NavigateToURL(shell(), target_url); |
+ } |
+ |
+ bool SimulateDragEnterToRightFrame(const std::string& text) { |
+ AssertTestPageIsLoaded(); |
+ return drag_simulator_->SimulateDragEnter(kMiddleOfRightFrame, text); |
+ } |
+ |
+ bool SimulateDropInRightFrame() { |
+ AssertTestPageIsLoaded(); |
+ return drag_simulator_->SimulateDrop(kMiddleOfRightFrame); |
+ } |
+ |
+ RenderFrameHost* right_frame() { |
+ AssertTestPageIsLoaded(); |
+ return GetFrameByName("right"); |
+ } |
+ |
+ private: |
+ RenderFrameHost* GetFrameByName(const std::string& name_to_find) { |
+ RenderFrameHost* result = nullptr; |
+ for (RenderFrameHost* rfh : shell()->web_contents()->GetAllFrames()) { |
+ if (rfh->GetFrameName() == name_to_find) { |
+ if (result) { |
+ ADD_FAILURE() << "More than one frame named " |
+ << "'" << name_to_find << "'"; |
+ return nullptr; |
+ } |
+ result = rfh; |
+ } |
+ } |
+ |
+ EXPECT_TRUE(result) << "Couldn't find a frame named " |
+ << "'" << name_to_find << "'"; |
+ return result; |
+ } |
+ |
+ void AssertTestPageIsLoaded() { |
+ ASSERT_EQ(kTestPagePath, |
+ shell()->web_contents()->GetLastCommittedURL().path()); |
+ } |
+ |
+ std::unique_ptr<DragAndDropSimulator> drag_simulator_; |
+ |
+ // Constants with coordinates within content/test/data/drag_and_drop/page.html |
+ const gfx::Point kMiddleOfLeftFrame = gfx::Point(200, 200); |
+ const gfx::Point kMiddleOfRightFrame = gfx::Point(400, 200); |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DragAndDropBrowserTest); |
+}; |
+ |
+IN_PROC_BROWSER_TEST_F(DragAndDropBrowserTest, DropTextFromOutside) { |
+ ASSERT_TRUE(NavigateToTestPage("a.com", |
+ "", // Left frame is unused by this test. |
+ "c.com/drag_and_drop/drop_target.html")); |
+ |
+ { |
+ std::unique_ptr<base::DictionaryValue> dragover_data; |
+ DOMDragEventWaiter dragover_waiter("dragover", right_frame()); |
+ ASSERT_TRUE(SimulateDragEnterToRightFrame("Dragged test text")); |
+ ASSERT_TRUE(dragover_waiter.WaitAndGetResponse(&dragover_data)); |
+ EXPECT_EQ("none", GetStringOrEmpty(*dragover_data, "drop_effect")); |
+ EXPECT_EQ("all", GetStringOrEmpty(*dragover_data, "effect_allowed")); |
+ EXPECT_EQ("text/plain", GetStringOrEmpty(*dragover_data, "mime_types")); |
+ } |
+ |
+ { |
+ std::unique_ptr<base::DictionaryValue> drop_data; |
+ DOMDragEventWaiter drop_waiter("drop", right_frame()); |
+ ASSERT_TRUE(SimulateDropInRightFrame()); |
+ ASSERT_TRUE(drop_waiter.WaitAndGetResponse(&drop_data)); |
+ EXPECT_EQ("none", GetStringOrEmpty(*drop_data, "drop_effect")); |
+ EXPECT_EQ("all", GetStringOrEmpty(*drop_data, "effect_allowed")); |
+ EXPECT_EQ("text/plain", GetStringOrEmpty(*drop_data, "mime_types")); |
+ } |
+} |
+ |
+} // namespace chrome |