1 // Copyright 2019 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 "components/paint_preview/renderer/paint_preview_recorder_impl.h"
6 
7 #include "base/files/file.h"
8 #include "base/files/file_path.h"
9 #include "base/files/scoped_temp_dir.h"
10 #include "base/test/scoped_feature_list.h"
11 #include "base/threading/thread_restrictions.h"
12 #include "build/build_config.h"
13 #include "components/paint_preview/common/file_stream.h"
14 #include "components/paint_preview/common/mojom/paint_preview_recorder.mojom.h"
15 #include "content/public/renderer/render_frame.h"
16 #include "content/public/renderer/render_view.h"
17 #include "content/public/test/render_view_test.h"
18 #include "content/public/test/test_utils.h"
19 #include "testing/gtest/include/gtest/gtest.h"
20 #include "third_party/blink/public/web/web_local_frame.h"
21 #include "third_party/skia/include/core/SkPicture.h"
22 #include "ui/native_theme/native_theme_features.h"
23 
24 namespace paint_preview {
25 
26 namespace {
27 
28 // Checks that |status| == |expected_status| and loads |response| into
29 // |out_response| if |expected_status| == kOk. If |expected_status| != kOk
30 // |out_response| can safely be nullptr.
OnCaptureFinished(mojom::PaintPreviewStatus expected_status,mojom::PaintPreviewCaptureResponsePtr * out_response,mojom::PaintPreviewStatus status,mojom::PaintPreviewCaptureResponsePtr response)31 void OnCaptureFinished(mojom::PaintPreviewStatus expected_status,
32                        mojom::PaintPreviewCaptureResponsePtr* out_response,
33                        mojom::PaintPreviewStatus status,
34                        mojom::PaintPreviewCaptureResponsePtr response) {
35   EXPECT_EQ(status, expected_status);
36   if (expected_status == mojom::PaintPreviewStatus::kOk)
37     *out_response = std::move(response);
38 }
39 
40 }  // namespace
41 
42 class PaintPreviewRecorderRenderViewTest : public content::RenderViewTest {
43  public:
PaintPreviewRecorderRenderViewTest()44   PaintPreviewRecorderRenderViewTest() {}
~PaintPreviewRecorderRenderViewTest()45   ~PaintPreviewRecorderRenderViewTest() override {}
46 
SetUp()47   void SetUp() override {
48     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
49 
50     // TODO(crbug/1022398): This is required to bypass a seemingly unrelated
51     // DCHECK for |use_overlay_scrollbars_| in NativeThemeAura on ChromeOS when
52     // painting scrollbars when first calling LoadHTML().
53     feature_list_.InitAndDisableFeature(features::kOverlayScrollbar);
54 
55     RenderViewTest::SetUp();
56   }
57 
GetFrame()58   content::RenderFrame* GetFrame() { return view_->GetMainRenderFrame(); }
59 
MakeTestFilePath(const std::string & filename)60   base::FilePath MakeTestFilePath(const std::string& filename) {
61     return temp_dir_.GetPath().AppendASCII(filename);
62   }
63 
RunCapture(content::RenderFrame * frame,mojom::PaintPreviewCaptureResponsePtr * out_response,bool is_main_frame=true,gfx::Rect clip_rect=gfx::Rect ())64   base::FilePath RunCapture(content::RenderFrame* frame,
65                             mojom::PaintPreviewCaptureResponsePtr* out_response,
66                             bool is_main_frame = true,
67                             gfx::Rect clip_rect = gfx::Rect()) {
68     base::FilePath skp_path = MakeTestFilePath("test.skp");
69 
70     mojom::PaintPreviewCaptureParamsPtr params =
71         mojom::PaintPreviewCaptureParams::New();
72     auto token = base::UnguessableToken::Create();
73     params->guid = token;
74     params->clip_rect = clip_rect;
75     params->clip_rect_is_hint = false;
76     params->is_main_frame = is_main_frame;
77     params->capture_links = true;
78     base::File skp_file(
79         skp_path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
80     params->file = std::move(skp_file);
81 
82     PaintPreviewRecorderImpl paint_preview_recorder(frame);
83     paint_preview_recorder.CapturePaintPreview(
84         std::move(params),
85         base::BindOnce(&OnCaptureFinished, mojom::PaintPreviewStatus::kOk,
86                        out_response));
87     content::RunAllTasksUntilIdle();
88     return skp_path;
89   }
90 
91  private:
92   base::ScopedTempDir temp_dir_;
93   base::test::ScopedFeatureList feature_list_;
94 };
95 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureMainFrameAndClipping)96 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndClipping) {
97   LoadHTML(
98       "<!doctype html>"
99       "<body>"
100       "  <div style='width: 600px; height: 80vh; "
101       "              background-color: #ff0000'>&nbsp;</div>"
102       "  <a style='display:inline-block' href='http://www.google.com'>Foo</a>"
103       "  <div style='width: 100px; height: 600px; "
104       "              background-color: #000000'>&nbsp;</div>"
105       "  <div style='overflow: hidden; width: 100px; height: 100px;"
106       "              background: orange;'>"
107       "    <div style='width: 500px; height: 500px;"
108       "                background: yellow;'></div>"
109       "  </div>"
110       "</body>");
111 
112   auto out_response = mojom::PaintPreviewCaptureResponse::New();
113   content::RenderFrame* frame = GetFrame();
114   base::FilePath skp_path = RunCapture(frame, &out_response);
115 
116   EXPECT_TRUE(out_response->embedding_token.has_value());
117   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
118             out_response->embedding_token.value());
119   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
120 
121   EXPECT_EQ(out_response->links.size(), 1U);
122   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.google.com/"));
123   // Relaxed checks on dimensions and no checks on positions. This is not
124   // intended to test the rendering behavior of the page only that a link
125   // was captured and has a bounding box.
126   EXPECT_GT(out_response->links[0]->rect.width(), 0);
127   EXPECT_GT(out_response->links[0]->rect.height(), 0);
128 
129   sk_sp<SkPicture> pic;
130   {
131     base::ScopedAllowBlockingForTesting scope;
132     FileRStream rstream(base::File(
133         skp_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ));
134     pic = SkPicture::MakeFromStream(&rstream, nullptr);
135   }
136   // The min page height is the sum of the three top level divs of 800. The min
137   // width is that of the widest div at 600.
138   EXPECT_GE(pic->cullRect().height(), 800);
139   EXPECT_GE(pic->cullRect().width(), 600);
140   SkBitmap bitmap;
141   ASSERT_TRUE(bitmap.tryAllocN32Pixels(pic->cullRect().width(),
142                                        pic->cullRect().height()));
143   SkCanvas canvas(bitmap, SkSurfaceProps{});
144   canvas.drawPicture(pic);
145   // This should be inside the top right corner of the first top level div.
146   // Success means there was no horizontal clipping as this region is red,
147   // matching the div.
148   EXPECT_EQ(bitmap.getColor(600, 50), 0xFFFF0000U);
149   // This should be inside the bottom of the second top level div. Success means
150   // there was no vertical clipping as this region is black matching the div. If
151   // the yellow div within the orange div overflowed then this would be yellow
152   // and fail.
153   EXPECT_EQ(bitmap.getColor(50, pic->cullRect().height() - 150), 0xFF000000U);
154   // This should be for the white background in the bottom right. This checks
155   // that the background is not clipped.
156   EXPECT_EQ(bitmap.getColor(pic->cullRect().width() - 50,
157                             pic->cullRect().height() - 50),
158             0xFFFFFFFFU);
159 }
160 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureMainFrameWithScroll)161 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameWithScroll) {
162   LoadHTML(
163       "<!doctype html>"
164       "<body>"
165       "  <div style='width: 600px; height: 80vh; "
166       "              background-color: #ff0000'>&nbsp;</div>"
167       "  <div style='width: 600px; height: 1200px; "
168       "              background-color: #00ff00'>&nbsp;</div>"
169       "</body>");
170 
171   // Scroll to bottom of page to ensure scroll position has no effect on
172   // capture.
173   ExecuteJavaScriptForTests("window.scrollTo(0,document.body.scrollHeight);");
174 
175   auto out_response = mojom::PaintPreviewCaptureResponse::New();
176   content::RenderFrame* frame = GetFrame();
177   base::FilePath skp_path = RunCapture(frame, &out_response);
178 
179   EXPECT_TRUE(out_response->embedding_token.has_value());
180   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
181             out_response->embedding_token.value());
182   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
183 
184   // Relaxed checks on dimensions and no checks on positions. This is not
185   // intended to intensively test the rendering behavior of the page.
186   sk_sp<SkPicture> pic;
187   {
188     base::ScopedAllowBlockingForTesting scope;
189     FileRStream rstream(base::File(
190         skp_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ));
191     pic = SkPicture::MakeFromStream(&rstream, nullptr);
192   }
193   SkBitmap bitmap;
194   ASSERT_TRUE(bitmap.tryAllocN32Pixels(pic->cullRect().width(),
195                                        pic->cullRect().height()));
196   SkCanvas canvas(bitmap, SkSurfaceProps{});
197   canvas.drawPicture(pic);
198   // This should be inside the top right corner of the top div. Success means
199   // there was no horizontal or vertical clipping as this region is red,
200   // matching the div.
201   EXPECT_EQ(bitmap.getColor(600, 50), 0xFFFF0000U);
202   // This should be inside the bottom of the bottom div. Success means there was
203   // no vertical clipping as this region is blue matching the div.
204   EXPECT_EQ(bitmap.getColor(50, pic->cullRect().height() - 100), 0xFF00FF00U);
205 }
206 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureFragment)207 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureFragment) {
208   // Use position absolute position to check that the captured link dimensions
209   // match what is specified.
210   LoadHTML(
211       "<!doctype html>"
212       "<body style='min-height:1000px;'>"
213       "  <a style='position: absolute; left: -15px; top: 0px; width: 40px; "
214       "   height: 30px;' href='#fragment'>Foo</a>"
215       "  <h1 id='fragment'>I'm a fragment</h1>"
216       "</body>");
217   auto out_response = mojom::PaintPreviewCaptureResponse::New();
218   content::RenderFrame* frame = GetFrame();
219 
220   RunCapture(frame, &out_response);
221 
222   EXPECT_TRUE(out_response->embedding_token.has_value());
223   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
224             out_response->embedding_token.value());
225   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
226 
227   EXPECT_EQ(out_response->links.size(), 1U);
228   EXPECT_EQ(out_response->links[0]->url, GURL("fragment"));
229   EXPECT_EQ(out_response->links[0]->rect.x(), -15);
230   EXPECT_EQ(out_response->links[0]->rect.y(), 0);
231   EXPECT_EQ(out_response->links[0]->rect.width(), 40);
232   EXPECT_EQ(out_response->links[0]->rect.height(), 30);
233 }
234 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureInvalidFile)235 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureInvalidFile) {
236   LoadHTML("<body></body>");
237 
238   mojom::PaintPreviewCaptureParamsPtr params =
239       mojom::PaintPreviewCaptureParams::New();
240   auto token = base::UnguessableToken::Create();
241   params->guid = token;
242   params->clip_rect = gfx::Rect();
243   params->is_main_frame = true;
244   params->capture_links = true;
245   params->max_capture_size = 0;
246   base::File skp_file;  // Invalid file.
247   params->file = std::move(skp_file);
248 
249   content::RenderFrame* frame = GetFrame();
250   PaintPreviewRecorderImpl paint_preview_recorder(frame);
251   paint_preview_recorder.CapturePaintPreview(
252       std::move(params),
253       base::BindOnce(&OnCaptureFinished,
254                      mojom::PaintPreviewStatus::kCaptureFailed, nullptr));
255   content::RunAllTasksUntilIdle();
256 }
257 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureMainFrameAndLocalFrame)258 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndLocalFrame) {
259   LoadHTML(
260       "<!doctype html>"
261       "<body style='min-height:1000px;'>"
262       "  <iframe style='width: 500px, height: 500px'"
263       "          srcdoc=\"<div style='width: 100px; height: 100px;"
264       "          background-color: #000000'>&nbsp;</div>\"></iframe>"
265       "</body>");
266   auto out_response = mojom::PaintPreviewCaptureResponse::New();
267   content::RenderFrame* frame = GetFrame();
268 
269   RunCapture(frame, &out_response);
270 
271   EXPECT_TRUE(out_response->embedding_token.has_value());
272   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
273             out_response->embedding_token.value());
274   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
275 }
276 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureLocalFrame)277 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureLocalFrame) {
278   LoadHTML(
279       "<!doctype html>"
280       "<body style='min-height:1000px;'>"
281       "  <iframe style='width: 500px, height: 500px'"
282       "          srcdoc=\"<div style='width: 100px; height: 100px;"
283       "          background-color: #000000'>&nbsp;</div>\"></iframe>"
284       "</body>");
285   auto out_response = mojom::PaintPreviewCaptureResponse::New();
286   auto* child_frame = content::RenderFrame::FromWebFrame(
287       GetFrame()->GetWebFrame()->FirstChild()->ToWebLocalFrame());
288   ASSERT_TRUE(child_frame);
289 
290   RunCapture(child_frame, &out_response, false);
291 
292   EXPECT_TRUE(out_response->embedding_token.has_value());
293   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
294 }
295 
TEST_F(PaintPreviewRecorderRenderViewTest,TestCaptureCustomClipRect)296 TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureCustomClipRect) {
297   LoadHTML(
298       "<!doctype html>"
299       "<body>"
300       "  <div style='width: 600px; height: 600px; background-color: #0000ff;'>"
301       "     <div style='width: 300px; height: 300px; background-color: "
302       "          #ffff00; position: relative; left: 150px; top: 150px'></div>"
303       "  </div>"
304       "  <a style='position: absolute; left: 160px; top: 170px; width: 40px; "
305       "   height: 30px;' href='http://www.example.com'>Foo</a>"
306       "</body>");
307 
308   auto out_response = mojom::PaintPreviewCaptureResponse::New();
309   content::RenderFrame* frame = GetFrame();
310   gfx::Rect clip_rect = gfx::Rect(150, 150, 300, 300);
311   base::FilePath skp_path = RunCapture(frame, &out_response, true, clip_rect);
312 
313   EXPECT_TRUE(out_response->embedding_token.has_value());
314   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
315             out_response->embedding_token.value());
316   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
317 
318   sk_sp<SkPicture> pic;
319   {
320     base::ScopedAllowBlockingForTesting scope;
321     FileRStream rstream(base::File(
322         skp_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ));
323     pic = SkPicture::MakeFromStream(&rstream, nullptr);
324   }
325   EXPECT_EQ(pic->cullRect().height(), 300);
326   EXPECT_EQ(pic->cullRect().width(), 300);
327   SkBitmap bitmap;
328   ASSERT_TRUE(bitmap.tryAllocN32Pixels(pic->cullRect().width(),
329                                        pic->cullRect().height()));
330   SkCanvas canvas(bitmap);
331   canvas.drawPicture(pic);
332   EXPECT_EQ(bitmap.getColor(100, 100), 0xFFFFFF00U);
333 
334   ASSERT_EQ(out_response->links.size(), 1U);
335   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.example.com"));
336   EXPECT_EQ(out_response->links[0]->rect.x(), 10);
337   EXPECT_EQ(out_response->links[0]->rect.y(), 20);
338   EXPECT_EQ(out_response->links[0]->rect.width(), 40);
339   EXPECT_EQ(out_response->links[0]->rect.height(), 30);
340 }
341 
TEST_F(PaintPreviewRecorderRenderViewTest,CaptureWithTranslate)342 TEST_F(PaintPreviewRecorderRenderViewTest, CaptureWithTranslate) {
343   // URLs should be annotated correctly when a CSS transform is applied.
344   LoadHTML(
345       R"(
346       <!doctype html>
347       <body>
348       <div style="display: inline-block;
349                   padding: 16px;
350                   font-size: 16px;">
351         <div style="padding: 16px;
352                     transform: translate(10px, 20px);
353                     margin-bottom: 30px;">
354           <div>
355             <a href="http://www.example.com" style="display: block;
356                                                     width: 70px;
357                                                     height: 20px;">
358               <div>Example</div>
359             </a>
360           </div>
361         </div>
362       </div>
363     </body>)");
364   auto out_response = mojom::PaintPreviewCaptureResponse::New();
365   content::RenderFrame* frame = GetFrame();
366 
367   RunCapture(frame, &out_response);
368 
369   EXPECT_TRUE(out_response->embedding_token.has_value());
370   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
371             out_response->embedding_token.value());
372   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
373 
374   ASSERT_EQ(out_response->links.size(), 1U);
375   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.example.com"));
376   EXPECT_NEAR(out_response->links[0]->rect.x(), 50, 3);
377   EXPECT_NEAR(out_response->links[0]->rect.y(), 60, 3);
378   EXPECT_NEAR(out_response->links[0]->rect.width(), 70, 3);
379   EXPECT_NEAR(out_response->links[0]->rect.height(), 20, 3);
380 }
381 
TEST_F(PaintPreviewRecorderRenderViewTest,CaptureWithTranslateThenRotate)382 TEST_F(PaintPreviewRecorderRenderViewTest, CaptureWithTranslateThenRotate) {
383   // URLs should be annotated correctly when a CSS transform is applied.
384   LoadHTML(
385       R"(
386       <!doctype html>
387       <body>
388       <div style="display: inline-block;
389                   padding: 16px;
390                   font-size: 16px;">
391         <div style="padding: 16px;
392                     transform: translate(100px, 0) rotate(45deg);
393                     margin-bottom: 30px;">
394           <div>
395             <a href="http://www.example.com" style="display: block;
396                                                     width: 70px;
397                                                     height: 20px;">
398               <div>Example</div>
399             </a>
400           </div>
401         </div>
402       </div>
403     </body>)");
404   auto out_response = mojom::PaintPreviewCaptureResponse::New();
405   content::RenderFrame* frame = GetFrame();
406 
407   RunCapture(frame, &out_response);
408 
409   EXPECT_TRUE(out_response->embedding_token.has_value());
410   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
411             out_response->embedding_token.value());
412   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
413 
414   ASSERT_EQ(out_response->links.size(), 1U);
415   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.example.com"));
416   EXPECT_NEAR(out_response->links[0]->rect.x(), 141, 5);
417   EXPECT_NEAR(out_response->links[0]->rect.y(), 18, 5);
418 #if !defined(OS_ANDROID)
419   EXPECT_NEAR(out_response->links[0]->rect.width(), 58, 10);
420   EXPECT_NEAR(out_response->links[0]->rect.height(), 58, 10);
421 #endif
422 }
423 
TEST_F(PaintPreviewRecorderRenderViewTest,CaptureWithRotateThenTranslate)424 TEST_F(PaintPreviewRecorderRenderViewTest, CaptureWithRotateThenTranslate) {
425   // URLs should be annotated correctly when a CSS transform is applied.
426   LoadHTML(
427       R"(
428       <!doctype html>
429       <body>
430       <div style="display: inline-block;
431                   padding: 16px;
432                   font-size: 16px;">
433         <div style="padding: 16px;
434                     transform: rotate(45deg) translate(100px, 0);
435                     margin-bottom: 30px;">
436           <div>
437             <a href="http://www.example.com" style="display: block;
438                                                     width: 70px;
439                                                     height: 20px;">
440               <div>Example</div>
441             </a>
442           </div>
443         </div>
444       </div>
445     </body>)");
446   auto out_response = mojom::PaintPreviewCaptureResponse::New();
447   content::RenderFrame* frame = GetFrame();
448 
449   RunCapture(frame, &out_response);
450 
451   EXPECT_TRUE(out_response->embedding_token.has_value());
452   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
453             out_response->embedding_token.value());
454   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
455 
456   ASSERT_EQ(out_response->links.size(), 1U);
457   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.example.com"));
458   EXPECT_NEAR(out_response->links[0]->rect.x(), 111, 5);
459   EXPECT_NEAR(out_response->links[0]->rect.y(), 88, 5);
460 #if !defined(OS_ANDROID)
461   EXPECT_NEAR(out_response->links[0]->rect.width(), 58, 10);
462   EXPECT_NEAR(out_response->links[0]->rect.height(), 58, 10);
463 #endif
464 }
465 
TEST_F(PaintPreviewRecorderRenderViewTest,CaptureWithScale)466 TEST_F(PaintPreviewRecorderRenderViewTest, CaptureWithScale) {
467   // URLs should be annotated correctly when a CSS transform is applied.
468   LoadHTML(
469       R"(
470       <!doctype html>
471       <body>
472       <div style="display: inline-block;
473                   padding: 16px;
474                   font-size: 16px;">
475         <div style="padding: 16px;
476                     transform: scale(2, 1);
477                     margin-bottom: 30px;">
478           <div>
479             <a href="http://www.example.com" style="display: block;
480                                                     width: 70px;
481                                                     height: 20px;">
482               <div>Example</div>
483             </a>
484           </div>
485         </div>
486       </div>
487     </body>)");
488   auto out_response = mojom::PaintPreviewCaptureResponse::New();
489   content::RenderFrame* frame = GetFrame();
490 
491   RunCapture(frame, &out_response);
492 
493   EXPECT_TRUE(out_response->embedding_token.has_value());
494   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
495             out_response->embedding_token.value());
496   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
497 
498   ASSERT_EQ(out_response->links.size(), 1U);
499   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.example.com"));
500   EXPECT_NEAR(out_response->links[0]->rect.x(), 5, 3);
501   EXPECT_NEAR(out_response->links[0]->rect.y(), 40, 3);
502   EXPECT_NEAR(out_response->links[0]->rect.width(), 140, 3);
503   EXPECT_NEAR(out_response->links[0]->rect.height(), 20, 3);
504 }
505 
TEST_F(PaintPreviewRecorderRenderViewTest,CaptureSaveRestore)506 TEST_F(PaintPreviewRecorderRenderViewTest, CaptureSaveRestore) {
507   // URLs should be annotated correctly when a CSS transform is applied.
508   LoadHTML(
509       R"(
510       <!doctype html>
511       <body>
512       <div style="display: inline-block;
513                   padding: 16px;
514                   font-size: 16px;">
515         <div style="padding: 16px;
516                     transform: translate(20px, 0);
517                     margin-bottom: 30px;">
518           <div>
519             <a href="http://www.example.com" style="display: block;
520                                                     width: 70px;
521                                                     height: 20px;">
522               <div>Example</div>
523             </a>
524           </div>
525         </div>
526         <div style="padding: 16px;
527                     transform: none;
528                     margin-bottom: 30px;">
529           <div>
530             <a href="http://www.chromium.org" style="display: block;
531                                                      width: 80px;
532                                                      height: 20px;">
533               <div>Chromium</div>
534             </a>
535           </div>
536         </div>
537       </div>
538     </body>)");
539   auto out_response = mojom::PaintPreviewCaptureResponse::New();
540   content::RenderFrame* frame = GetFrame();
541 
542   RunCapture(frame, &out_response);
543 
544   EXPECT_TRUE(out_response->embedding_token.has_value());
545   EXPECT_EQ(frame->GetWebFrame()->GetEmbeddingToken(),
546             out_response->embedding_token.value());
547   EXPECT_EQ(out_response->content_id_to_embedding_token.size(), 0U);
548 
549   ASSERT_EQ(out_response->links.size(), 2U);
550   EXPECT_EQ(out_response->links[0]->url, GURL("http://www.chromium.org"));
551   EXPECT_NEAR(out_response->links[0]->rect.x(), 40, 3);
552   EXPECT_NEAR(out_response->links[0]->rect.y(), 122, 3);
553   EXPECT_NEAR(out_response->links[0]->rect.width(), 80, 3);
554   EXPECT_NEAR(out_response->links[0]->rect.height(), 20, 3);
555 
556   EXPECT_EQ(out_response->links[1]->url, GURL("http://www.example.com"));
557   EXPECT_NEAR(out_response->links[1]->rect.x(), 60, 3);
558   EXPECT_NEAR(out_response->links[1]->rect.y(), 40, 3);
559   EXPECT_NEAR(out_response->links[1]->rect.width(), 70, 3);
560   EXPECT_NEAR(out_response->links[1]->rect.height(), 20, 3);
561 }
562 
563 }  // namespace paint_preview
564