1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/core/clipboard/data_transfer.h"
6 
7 #include "testing/gtest/include/gtest/gtest.h"
8 #include "third_party/blink/renderer/core/dom/element.h"
9 #include "third_party/blink/renderer/core/frame/local_frame.h"
10 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
11 #include "third_party/blink/renderer/core/frame/settings.h"
12 #include "third_party/blink/renderer/core/layout/layout_object.h"
13 #include "third_party/blink/renderer/core/page/drag_image.h"
14 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
15 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
16 
17 namespace blink {
18 
19 class DataTransferTest : public RenderingTest {
20  protected:
GetPage() const21   Page& GetPage() const { return *GetDocument().GetPage(); }
GetFrame() const22   LocalFrame& GetFrame() const { return *GetDocument().GetFrame(); }
23 };
24 
TEST_F(DataTransferTest,NodeImage)25 TEST_F(DataTransferTest, NodeImage) {
26   SetBodyInnerHTML(R"HTML(
27     <style>
28       #sample { width: 100px; height: 100px; }
29     </style>
30     <div id=sample></div>
31   )HTML");
32   Element* sample = GetDocument().getElementById("sample");
33   const std::unique_ptr<DragImage> image =
34       DataTransfer::NodeImage(GetFrame(), *sample);
35   EXPECT_EQ(IntSize(100, 100), image->Size());
36 }
37 
TEST_F(DataTransferTest,NodeImageWithNestedElement)38 TEST_F(DataTransferTest, NodeImageWithNestedElement) {
39   SetBodyInnerHTML(R"HTML(
40     <style>
41       div { -webkit-user-drag: element }
42       span:-webkit-drag { color: #0F0 }
43     </style>
44     <div id=sample><span>Green when dragged</span></div>
45   )HTML");
46   Element* sample = GetDocument().getElementById("sample");
47   const std::unique_ptr<DragImage> image =
48       DataTransfer::NodeImage(GetFrame(), *sample);
49   EXPECT_EQ(Color(0, 255, 0),
50             sample->firstChild()->GetLayoutObject()->ResolveColor(
51                 GetCSSPropertyColor()))
52       << "Descendants node should have :-webkit-drag.";
53 }
54 
TEST_F(DataTransferTest,NodeImageWithPsuedoClassWebKitDrag)55 TEST_F(DataTransferTest, NodeImageWithPsuedoClassWebKitDrag) {
56   SetBodyInnerHTML(R"HTML(
57     <style>
58       #sample { width: 100px; height: 100px; }
59       #sample:-webkit-drag { width: 200px; height: 200px; }
60     </style>
61     <div id=sample></div>
62   )HTML");
63   Element* sample = GetDocument().getElementById("sample");
64   const std::unique_ptr<DragImage> image =
65       DataTransfer::NodeImage(GetFrame(), *sample);
66   EXPECT_EQ(IntSize(200, 200), image->Size())
67       << ":-webkit-drag should affect dragged image.";
68 }
69 
TEST_F(DataTransferTest,NodeImageWithoutDraggedLayoutObject)70 TEST_F(DataTransferTest, NodeImageWithoutDraggedLayoutObject) {
71   SetBodyInnerHTML(R"HTML(
72     <style>
73       #sample { width: 100px; height: 100px; }
74       #sample:-webkit-drag { display:none }
75     </style>
76     <div id=sample></div>
77   )HTML");
78   Element* sample = GetDocument().getElementById("sample");
79   const std::unique_ptr<DragImage> image =
80       DataTransfer::NodeImage(GetFrame(), *sample);
81   EXPECT_EQ(nullptr, image.get()) << ":-webkit-drag blows away layout object";
82 }
83 
TEST_F(DataTransferTest,NodeImageWithChangingLayoutObject)84 TEST_F(DataTransferTest, NodeImageWithChangingLayoutObject) {
85   SetBodyInnerHTML(R"HTML(
86     <style>
87       #sample { color: blue; }
88       #sample:-webkit-drag { display: inline-block; color: red; }
89     </style>
90     <span id=sample>foo</span>
91   )HTML");
92   Element* sample = GetDocument().getElementById("sample");
93   UpdateAllLifecyclePhasesForTest();
94   LayoutObject* before_layout_object = sample->GetLayoutObject();
95   const std::unique_ptr<DragImage> image =
96       DataTransfer::NodeImage(GetFrame(), *sample);
97 
98   EXPECT_TRUE(sample->GetLayoutObject() != before_layout_object)
99       << ":-webkit-drag causes sample to have different layout object.";
100   EXPECT_EQ(Color(255, 0, 0),
101             sample->GetLayoutObject()->ResolveColor(GetCSSPropertyColor()))
102       << "#sample has :-webkit-drag.";
103 
104   // Layout w/o :-webkit-drag
105   UpdateAllLifecyclePhasesForTest();
106 
107   EXPECT_EQ(Color(0, 0, 255),
108             sample->GetLayoutObject()->ResolveColor(GetCSSPropertyColor()))
109       << "#sample doesn't have :-webkit-drag.";
110 }
111 
TEST_F(DataTransferTest,NodeImageExceedsViewportBounds)112 TEST_F(DataTransferTest, NodeImageExceedsViewportBounds) {
113   SetBodyInnerHTML(R"HTML(
114     <style>
115       * { margin: 0; }
116       #node { width: 2000px; height: 2000px; }
117     </style>
118     <div id='node'></div>
119   )HTML");
120   Element& node = *GetDocument().getElementById("node");
121   const auto image = DataTransfer::NodeImage(GetFrame(), node);
122   EXPECT_EQ(IntSize(800, 600), image->Size());
123 }
124 
TEST_F(DataTransferTest,NodeImageUnderScrollOffset)125 TEST_F(DataTransferTest, NodeImageUnderScrollOffset) {
126   SetBodyInnerHTML(R"HTML(
127     <style>
128       * { margin: 0; }
129       #first { width: 500px; height: 500px; }
130       #second { width: 800px; height: 900px; }
131     </style>
132     <div id='first'></div>
133     <div id='second'></div>
134   )HTML");
135 
136   const int scroll_amount = 10;
137   LocalFrameView* frame_view = GetDocument().View();
138   frame_view->LayoutViewport()->SetScrollOffset(
139       ScrollOffset(0, scroll_amount), mojom::blink::ScrollType::kProgrammatic);
140 
141   // The first div should be offset by the scroll offset.
142   Element& first = *GetDocument().getElementById("first");
143   const auto first_image = DataTransfer::NodeImage(GetFrame(), first);
144   const int first_height = 500;
145   EXPECT_EQ(IntSize(500, first_height), first_image->Size());
146 
147   // The second div should also be offset by the scroll offset. In addition,
148   // the second div should be clipped by the viewport.
149   Element& second = *GetDocument().getElementById("second");
150   const auto second_image = DataTransfer::NodeImage(GetFrame(), second);
151   const int viewport_height = 600;
152   EXPECT_EQ(IntSize(800, viewport_height - (first_height - scroll_amount)),
153             second_image->Size());
154 }
155 
TEST_F(DataTransferTest,NodeImageSizeWithPageScaleFactor)156 TEST_F(DataTransferTest, NodeImageSizeWithPageScaleFactor) {
157   SetBodyInnerHTML(R"HTML(
158     <style>
159       * { margin: 0; }
160       html, body { height: 2000px; }
161       #node { width: 200px; height: 141px; }
162     </style>
163     <div id='node'></div>
164   )HTML");
165   const int page_scale_factor = 2;
166   GetPage().SetPageScaleFactor(page_scale_factor);
167   Element& node = *GetDocument().getElementById("node");
168   const auto image = DataTransfer::NodeImage(GetFrame(), node);
169   const int node_width = 200;
170   const int node_height = 141;
171   EXPECT_EQ(
172       IntSize(node_width * page_scale_factor, node_height * page_scale_factor),
173       image->Size());
174 
175   // Check that a scroll offset is scaled to device coordinates which includes
176   // page scale factor.
177   const int scroll_amount = 10;
178   LocalFrameView* frame_view = GetDocument().View();
179   frame_view->LayoutViewport()->SetScrollOffset(
180       ScrollOffset(0, scroll_amount), mojom::blink::ScrollType::kProgrammatic);
181   const auto image_with_offset = DataTransfer::NodeImage(GetFrame(), node);
182   EXPECT_EQ(
183       IntSize(node_width * page_scale_factor, node_height * page_scale_factor),
184       image_with_offset->Size());
185 }
186 
TEST_F(DataTransferTest,NodeImageSizeWithPageScaleFactorTooLarge)187 TEST_F(DataTransferTest, NodeImageSizeWithPageScaleFactorTooLarge) {
188   SetBodyInnerHTML(R"HTML(
189     <style>
190       * { margin: 0; }
191       html, body { height: 2000px; }
192       #node { width: 800px; height: 601px; }
193     </style>
194     <div id='node'></div>
195   )HTML");
196   const int page_scale_factor = 2;
197   GetPage().SetPageScaleFactor(page_scale_factor);
198   Element& node = *GetDocument().getElementById("node");
199   const auto image = DataTransfer::NodeImage(GetFrame(), node);
200   const int node_width = 800;
201   const int node_height = 601;
202   EXPECT_EQ(IntSize(node_width * page_scale_factor,
203                     (node_height - 1) * page_scale_factor),
204             image->Size());
205 
206   // Check that a scroll offset is scaled to device coordinates which includes
207   // page scale factor.
208   const int scroll_amount = 10;
209   LocalFrameView* frame_view = GetDocument().View();
210   frame_view->LayoutViewport()->SetScrollOffset(
211       ScrollOffset(0, scroll_amount), mojom::blink::ScrollType::kProgrammatic);
212   const auto image_with_offset = DataTransfer::NodeImage(GetFrame(), node);
213   EXPECT_EQ(IntSize(node_width * page_scale_factor,
214                     (node_height - scroll_amount) * page_scale_factor),
215             image_with_offset->Size());
216 }
217 
TEST_F(DataTransferTest,NodeImageWithPageScaleFactor)218 TEST_F(DataTransferTest, NodeImageWithPageScaleFactor) {
219   // #bluegreen is a 2x1 rectangle where the left pixel is blue and the right
220   // pixel is green. The element is offset by a margin of 1px.
221   SetBodyInnerHTML(R"HTML(
222     <style>
223       * { margin: 0; }
224       #bluegreen {
225         width: 1px;
226         height: 1px;
227         background: #0f0;
228         border-left: 1px solid #00f;
229         margin: 1px;
230       }
231     </style>
232     <div id='bluegreen'></div>
233   )HTML");
234   const int page_scale_factor = 2;
235   GetPage().SetPageScaleFactor(page_scale_factor);
236   Element& blue_green = *GetDocument().getElementById("bluegreen");
237   const auto image = DataTransfer::NodeImage(GetFrame(), blue_green);
238   const int blue_green_width = 2;
239   const int blue_green_height = 1;
240   EXPECT_EQ(IntSize(blue_green_width * page_scale_factor,
241                     blue_green_height * page_scale_factor),
242             image->Size());
243 
244   // Even though #bluegreen is offset by a margin of 1px (which is 2px in device
245   // coordinates), we expect it to be painted at 0x0 and completely fill the 4x2
246   // bitmap.
247   SkBitmap expected_bitmap;
248   expected_bitmap.allocN32Pixels(4, 2);
249   expected_bitmap.eraseArea(SkIRect::MakeXYWH(0, 0, 2, 2), 0xFF0000FF);
250   expected_bitmap.eraseArea(SkIRect::MakeXYWH(2, 0, 2, 2), 0xFF00FF00);
251   const SkBitmap& bitmap = image->Bitmap();
252   for (int x = 0; x < bitmap.width(); ++x)
253     for (int y = 0; y < bitmap.height(); ++y)
254       EXPECT_EQ(expected_bitmap.getColor(x, y), bitmap.getColor(x, y));
255 }
256 
TEST_F(DataTransferTest,NodeImageFullyOffscreen)257 TEST_F(DataTransferTest, NodeImageFullyOffscreen) {
258   SetBodyInnerHTML(R"HTML(
259     <style>
260     #target {
261       position: absolute;
262       top: 800px;
263       left: 0;
264       height: 100px;
265       width: 200px;
266       background: lightblue;
267       isolation: isolate;
268     }
269     </style>
270     <div id="target" draggable="true" ondragstart="drag(event)"></div>
271   )HTML");
272 
273   const int scroll_amount = 800;
274   LocalFrameView* frame_view = GetDocument().View();
275   frame_view->LayoutViewport()->SetScrollOffset(
276       ScrollOffset(0, scroll_amount), mojom::blink::ScrollType::kProgrammatic);
277 
278   Element& target = *GetDocument().getElementById("target");
279   const auto image = DataTransfer::NodeImage(GetFrame(), target);
280 
281   EXPECT_EQ(IntSize(200, 100), image->Size());
282 }
283 
TEST_F(DataTransferTest,NodeImageWithScrolling)284 TEST_F(DataTransferTest, NodeImageWithScrolling) {
285   SetBodyInnerHTML(R"HTML(
286     <style>
287     #target {
288       position: absolute;
289       top: 800px;
290       left: 0;
291       height: 100px;
292       width: 200px;
293       background: lightblue;
294       isolation: isolate;
295     }
296     </style>
297     <div id="target" draggable="true" ondragstart="drag(event)"></div>
298   )HTML");
299 
300   Element& target = *GetDocument().getElementById("target");
301   const auto image = DataTransfer::NodeImage(GetFrame(), target);
302 
303   EXPECT_EQ(IntSize(200, 100), image->Size());
304 }
305 
TEST_F(DataTransferTest,NodeImageInOffsetStackingContext)306 TEST_F(DataTransferTest, NodeImageInOffsetStackingContext) {
307   SetBodyInnerHTML(R"HTML(
308     <style>
309       * { margin: 0; }
310       #container {
311         position: absolute;
312         top: 4px;
313         z-index: 10;
314       }
315       #drag {
316         width: 5px;
317         height: 5px;
318         background: #0F0;
319       }
320     </style>
321     <div id="container">
322       <div id="drag" draggable="true"></div>
323     </div>
324   )HTML");
325   Element& drag = *GetDocument().getElementById("drag");
326   const auto image = DataTransfer::NodeImage(GetFrame(), drag);
327   constexpr int drag_width = 5;
328   constexpr int drag_height = 5;
329   EXPECT_EQ(IntSize(drag_width, drag_height), image->Size());
330 
331   // The dragged image should be (drag_width x drag_height) and fully green.
332   Color green = 0xFF00FF00;
333   const SkBitmap& bitmap = image->Bitmap();
334   for (int x = 0; x < drag_width; ++x) {
335     for (int y = 0; y < drag_height; ++y)
336       EXPECT_EQ(green, bitmap.getColor(x, y));
337   }
338 }
339 
TEST_F(DataTransferTest,NodeImageWithLargerPositionedDescendant)340 TEST_F(DataTransferTest, NodeImageWithLargerPositionedDescendant) {
341   SetBodyInnerHTML(R"HTML(
342     <style>
343       * { margin: 0; }
344       #drag {
345         position: absolute;
346         top: 100px;
347         left: 0;
348         height: 1px;
349         width: 1px;
350         background: #00f;
351       }
352       #child {
353         position: absolute;
354         top: -1px;
355         left: 0;
356         height: 3px;
357         width: 1px;
358         background: #0f0;
359       }
360     </style>
361     <div id="drag" draggable="true">
362       <div id="child"></div>
363     </div>
364   )HTML");
365   Element& drag = *GetDocument().getElementById("drag");
366   const auto image = DataTransfer::NodeImage(GetFrame(), drag);
367 
368   // The positioned #child should expand the dragged image's size.
369   constexpr int drag_width = 1;
370   constexpr int drag_height = 3;
371   EXPECT_EQ(IntSize(drag_width, drag_height), image->Size());
372 
373   // The dragged image should be (drag_width x drag_height) and fully green
374   // which is the color of the #child which fully covers the dragged element.
375   Color green = 0xFF00FF00;
376   const SkBitmap& bitmap = image->Bitmap();
377   for (int x = 0; x < drag_width; ++x) {
378     for (int y = 0; y < drag_height; ++y)
379       EXPECT_EQ(green, bitmap.getColor(x, y));
380   }
381 }
382 
383 }  // namespace blink
384