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/services/paint_preview_compositor/paint_preview_compositor_impl.h"
6 
7 #include <stdint.h>
8 
9 #include <memory>
10 #include <utility>
11 
12 #include "base/callback_helpers.h"
13 #include "base/containers/flat_map.h"
14 #include "base/containers/span.h"
15 #include "base/files/file.h"
16 #include "base/files/file_path.h"
17 #include "base/files/scoped_temp_dir.h"
18 #include "base/notreached.h"
19 #include "base/optional.h"
20 #include "base/test/task_environment.h"
21 #include "base/unguessable_token.h"
22 #include "components/paint_preview/common/capture_result.h"
23 #include "components/paint_preview/common/file_stream.h"
24 #include "components/paint_preview/common/paint_preview_tracker.h"
25 #include "components/paint_preview/common/proto/paint_preview.pb.h"
26 #include "components/paint_preview/common/recording_map.h"
27 #include "components/paint_preview/common/serialized_recording.h"
28 #include "mojo/public/cpp/base/big_buffer.h"
29 #include "testing/gmock/include/gmock/gmock.h"
30 #include "testing/gtest/include/gtest/gtest.h"
31 #include "third_party/skia/include/core/SkCanvas.h"
32 #include "third_party/skia/include/core/SkColor.h"
33 #include "third_party/skia/include/core/SkMatrix.h"
34 #include "third_party/skia/include/core/SkPicture.h"
35 #include "third_party/skia/include/core/SkPictureRecorder.h"
36 #include "third_party/skia/include/core/SkRefCnt.h"
37 #include "third_party/skia/include/core/SkStream.h"
38 #include "ui/gfx/geometry/rect.h"
39 
40 namespace paint_preview {
41 
42 namespace {
43 
44 // Checks that |status| == |expected_status|. If |expected_status| == kSuccess,
45 // then also checks that;
46 // - |response->root_frame_guid| == |expected_root_frame_guid|
47 // - |response->subframe_rect_hierarchy| == |expected_data|
BeginSeparatedFrameCompositeCallbackImpl(mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,const base::UnguessableToken & expected_root_frame_guid,const base::flat_map<base::UnguessableToken,mojom::FrameDataPtr> & expected_data,mojom::PaintPreviewCompositor::BeginCompositeStatus status,mojom::PaintPreviewBeginCompositeResponsePtr response)48 void BeginSeparatedFrameCompositeCallbackImpl(
49     mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,
50     const base::UnguessableToken& expected_root_frame_guid,
51     const base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>&
52         expected_data,
53     mojom::PaintPreviewCompositor::BeginCompositeStatus status,
54     mojom::PaintPreviewBeginCompositeResponsePtr response) {
55   EXPECT_EQ(status, expected_status);
56   if (expected_status !=
57       mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess)
58     return;
59   EXPECT_EQ(response->root_frame_guid, expected_root_frame_guid);
60   EXPECT_EQ(response->frames.size(), expected_data.size());
61   for (const auto& frame : expected_data) {
62     EXPECT_TRUE(response->frames.count(frame.first));
63     EXPECT_EQ(response->frames[frame.first]->scroll_extents,
64               frame.second->scroll_extents);
65     size_t size = response->frames[frame.first]->subframes.size();
66     EXPECT_EQ(size, frame.second->subframes.size());
67     std::vector<std::pair<base::UnguessableToken, gfx::Rect>>
68         response_subframes, expected_subframes;
69     for (size_t i = 0; i < size; ++i) {
70       response_subframes.push_back(
71           {response->frames[frame.first]->subframes[i]->frame_guid,
72            response->frames[frame.first]->subframes[i]->clip_rect});
73       expected_subframes.push_back({frame.second->subframes[i]->frame_guid,
74                                     frame.second->subframes[i]->clip_rect});
75     }
76     EXPECT_THAT(response_subframes,
77                 ::testing::UnorderedElementsAreArray(expected_subframes));
78   }
79 }
80 
BeginMainFrameCompositeCallbackImpl(mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,mojom::PaintPreviewCompositor::BeginCompositeStatus status)81 void BeginMainFrameCompositeCallbackImpl(
82     mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,
83     mojom::PaintPreviewCompositor::BeginCompositeStatus status) {
84   EXPECT_EQ(status, expected_status);
85 }
86 
87 // Checks that |status| == |expected_status|. If |expected_status| == kSuccess,
88 // then it also checks that |bitmap| and |expected_bitmap| are pixel identical.
BitmapCallbackImpl(mojom::PaintPreviewCompositor::BitmapStatus expected_status,const SkBitmap & expected_bitmap,mojom::PaintPreviewCompositor::BitmapStatus status,const SkBitmap & bitmap)89 void BitmapCallbackImpl(
90     mojom::PaintPreviewCompositor::BitmapStatus expected_status,
91     const SkBitmap& expected_bitmap,
92     mojom::PaintPreviewCompositor::BitmapStatus status,
93     const SkBitmap& bitmap) {
94   EXPECT_EQ(status, expected_status);
95   if (expected_status != mojom::PaintPreviewCompositor::BitmapStatus::kSuccess)
96     return;
97   EXPECT_EQ(bitmap.width(), expected_bitmap.width());
98   EXPECT_EQ(bitmap.height(), expected_bitmap.height());
99   EXPECT_EQ(bitmap.bytesPerPixel(), expected_bitmap.bytesPerPixel());
100   // Assert that all the bytes of the backing memory are equal. This check is
101   // only safe if all of the width, height and bytesPerPixel are equal between
102   // the two bitmaps.
103   EXPECT_EQ(memcmp(bitmap.getPixels(), expected_bitmap.getPixels(),
104                    expected_bitmap.bytesPerPixel() * expected_bitmap.width() *
105                        expected_bitmap.height()),
106             0);
107 }
108 
109 // Encodes |proto| a ReadOnlySharedMemoryRegion.
ToReadOnlySharedMemory(const PaintPreviewProto & proto)110 base::ReadOnlySharedMemoryRegion ToReadOnlySharedMemory(
111     const PaintPreviewProto& proto) {
112   auto region = base::WritableSharedMemoryRegion::Create(proto.ByteSizeLong());
113   EXPECT_TRUE(region.IsValid());
114   auto mapping = region.Map();
115   EXPECT_TRUE(mapping.IsValid());
116   proto.SerializeToArray(mapping.memory(), mapping.size());
117   return base::WritableSharedMemoryRegion::ConvertToReadOnly(std::move(region));
118 }
119 
ToSkRect(const gfx::Size & size)120 SkRect ToSkRect(const gfx::Size& size) {
121   return SkRect::MakeWH(size.width(), size.height());
122 }
123 
ToSkRect(const gfx::Rect & rect)124 SkRect ToSkRect(const gfx::Rect& rect) {
125   return SkRect::MakeXYWH(rect.x(), rect.y(), rect.width(), rect.height());
126 }
127 
128 // Draw a dummy picture of size |scroll_extents|, whose origin is equal to
129 // |clip_rect| and clipped by |clip_rect|'s size. The dummy picture will by
130 // filled with |rect_fill_color| with a cyan border and will have an XY axis
131 // drawn at the origin in red and green that is 10 units in |canvas|'s local
132 // coordinate system.
DrawDummyTestPicture(SkCanvas * canvas,SkColor rect_fill_color,const gfx::Size & scroll_extents,base::Optional<gfx::Rect> clip_rect=base::nullopt,gfx::Size scroll_offsets=gfx::Size ())133 void DrawDummyTestPicture(SkCanvas* canvas,
134                           SkColor rect_fill_color,
135                           const gfx::Size& scroll_extents,
136                           base::Optional<gfx::Rect> clip_rect = base::nullopt,
137                           gfx::Size scroll_offsets = gfx::Size()) {
138   canvas->save();
139   if (clip_rect.has_value()) {
140     canvas->clipRect(ToSkRect(*clip_rect));
141     canvas->translate(clip_rect->x(), clip_rect->y());
142   }
143   canvas->translate(-scroll_offsets.width(), -scroll_offsets.height());
144 
145   SkPaint paint;
146   paint.setStyle(SkPaint::kFill_Style);
147   paint.setColor(rect_fill_color);
148   canvas->drawRect(ToSkRect(scroll_extents), paint);
149 
150   SkPaint border_paint;
151   border_paint.setStyle(SkPaint::kStroke_Style);
152   border_paint.setColor(SK_ColorCYAN);
153   border_paint.setStrokeWidth(2.0);
154   canvas->drawRect(ToSkRect(scroll_extents), border_paint);
155 
156   const int kAxisLength = 25;
157   const int kAxisThickness = 1;
158 
159   // Draw axis as rects instead of lines so when the canvas is scaled, the axis
160   // scale relative to the origin, rather than by their stroke center.
161   SkPaint x_axis_paint;
162   x_axis_paint.setStyle(SkPaint::kFill_Style);
163   x_axis_paint.setColor(SK_ColorRED);
164   canvas->drawRect(SkRect::MakeXYWH(1, 0, kAxisLength, kAxisThickness),
165                    x_axis_paint);
166 
167   SkPaint y_axis_paint;
168   y_axis_paint.setStyle(SkPaint::kFill_Style);
169   y_axis_paint.setColor(SK_ColorGREEN);
170   canvas->drawRect(SkRect::MakeXYWH(0, 1, kAxisThickness, kAxisLength),
171                    y_axis_paint);
172 
173   // Draw an additional diagonal line to help identify the origin if a subframe
174   // is scrolled.
175   SkPaint diagonal_line_paint;
176   border_paint.setStyle(SkPaint::kStroke_Style);
177   border_paint.setColor(SK_ColorBLUE);
178   border_paint.setStrokeWidth(2.0);
179   canvas->drawLine(0, 0, kAxisLength, kAxisLength, diagonal_line_paint);
180 
181   canvas->restore();
182 }
183 
184 // Fill |frame| and |expected_data| with the remaining parameters and draw and
185 // save a recording to |path|.
PopulateFrameProto(PaintPreviewFrameProto * frame_proto,const base::UnguessableToken & guid,bool set_is_main_frame,const base::FilePath & path,const gfx::Size & scroll_extents,std::vector<std::pair<base::UnguessableToken,gfx::Rect>> subframes,base::flat_map<base::UnguessableToken,mojom::FrameDataPtr> * expected_data,gfx::Size scroll_offsets=gfx::Size (),SkColor picture_fill_color=SK_ColorDKGRAY)186 void PopulateFrameProto(
187     PaintPreviewFrameProto* frame_proto,
188     const base::UnguessableToken& guid,
189     bool set_is_main_frame,
190     const base::FilePath& path,
191     const gfx::Size& scroll_extents,
192     std::vector<std::pair<base::UnguessableToken, gfx::Rect>> subframes,
193     base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>* expected_data,
194     gfx::Size scroll_offsets = gfx::Size(),
195     SkColor picture_fill_color = SK_ColorDKGRAY) {
196   frame_proto->set_embedding_token_low(guid.GetLowForSerialization());
197   frame_proto->set_embedding_token_high(guid.GetHighForSerialization());
198   frame_proto->set_is_main_frame(set_is_main_frame);
199   frame_proto->set_file_path(path.AsUTF8Unsafe());
200   frame_proto->set_scroll_offset_x(scroll_offsets.width());
201   frame_proto->set_scroll_offset_y(scroll_offsets.height());
202 
203   SkPictureRecorder recorder;
204   SkCanvas* canvas = recorder.beginRecording(ToSkRect(scroll_extents));
205   DrawDummyTestPicture(canvas, picture_fill_color, scroll_extents);
206 
207   // It's okay to create a new document guid every time since we're not
208   // observing it.
209   PaintPreviewTracker tracker(base::UnguessableToken::Create(), guid,
210                               set_is_main_frame);
211   mojom::FrameDataPtr expected_frame_data = mojom::FrameData::New();
212   expected_frame_data->scroll_extents = scroll_extents;
213   expected_frame_data->scroll_offsets = scroll_offsets;
214 
215   for (const auto& subframe : subframes) {
216     const base::UnguessableToken& subframe_id = subframe.first;
217     gfx::Rect clip_rect = subframe.second;
218 
219     // Record the subframe as custom data to |canvas|.
220     uint32_t content_id =
221         tracker.CreateContentForRemoteFrame(clip_rect, subframe_id);
222     tracker.CustomDataToSkPictureCallback(canvas, content_id);
223 
224     auto* content_id_embedding_token_pair =
225         frame_proto->add_content_id_to_embedding_tokens();
226     content_id_embedding_token_pair->set_content_id(content_id);
227     content_id_embedding_token_pair->set_embedding_token_low(
228         subframe_id.GetLowForSerialization());
229     content_id_embedding_token_pair->set_embedding_token_high(
230         subframe_id.GetHighForSerialization());
231     expected_frame_data->subframes.push_back(
232         mojom::SubframeClipRect::New(subframe_id, clip_rect));
233     tracker.TransformClipForFrame(content_id);
234   }
235 
236   sk_sp<SkPicture> pic = recorder.finishRecordingAsPicture();
237 
238   size_t serialized_size = 0;
239   ASSERT_TRUE(RecordToFile(
240       base::File(path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE),
241       pic, &tracker, base::nullopt, &serialized_size));
242   ASSERT_GE(serialized_size, 0u);
243 
244   expected_data->insert({guid, std::move(expected_frame_data)});
245 }
246 
247 // Enumeration to select between the |*SeparateFrame*| and |*MainFrame*|
248 // functions on |PaintPreviewCompositor|.
249 enum class CompositeType { kSeparateFrame, kMainFrame };
250 
CompositeTypeParamToString(const::testing::TestParamInfo<CompositeType> & composite_type)251 std::string CompositeTypeParamToString(
252     const ::testing::TestParamInfo<CompositeType>& composite_type) {
253   switch (composite_type.param) {
254     case CompositeType::kSeparateFrame:
255       return "SeparateFrame";
256     case CompositeType::kMainFrame:
257       return "MainFrame";
258   }
259 }
260 
261 }  // namespace
262 
263 class PaintPreviewCompositorBeginCompositeTest
264     : public testing::TestWithParam<CompositeType> {
265  protected:
SetUp()266   void SetUp() override {
267     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
268     compositor_.SetRootFrameUrl(url_);
269   }
270 
271   // Run |Begin*Composite| with |request| and compare the response with
272   // the |expected_*| parameters.
BeginCompositeAndValidate(mojom::PaintPreviewBeginCompositeRequestPtr request,mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,const base::UnguessableToken & expected_root_frame_guid,base::flat_map<base::UnguessableToken,mojom::FrameDataPtr> expected_data)273   void BeginCompositeAndValidate(
274       mojom::PaintPreviewBeginCompositeRequestPtr request,
275       mojom::PaintPreviewCompositor::BeginCompositeStatus expected_status,
276       const base::UnguessableToken& expected_root_frame_guid,
277       base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>
278           expected_data) {
279     switch (GetParam()) {
280       case CompositeType::kSeparateFrame:
281         compositor_.BeginSeparatedFrameComposite(
282             std::move(request),
283             base::BindOnce(&BeginSeparatedFrameCompositeCallbackImpl,
284                            expected_status, expected_root_frame_guid,
285                            std::move(expected_data)));
286         break;
287       case CompositeType::kMainFrame:
288         compositor_.BeginMainFrameComposite(
289             std::move(request),
290             base::BindOnce(&BeginMainFrameCompositeCallbackImpl,
291                            expected_status));
292         break;
293       default:
294         NOTREACHED();
295         break;
296     }
297   }
298 
299   base::ScopedTempDir temp_dir_;
300 
301   GURL url_{"https://www.chromium.org"};
302 
303  private:
304   PaintPreviewCompositorImpl compositor_{mojo::NullReceiver(), nullptr,
305                                          base::DoNothing()};
306 };
307 
TEST_P(PaintPreviewCompositorBeginCompositeTest,MissingSubFrameRecording)308 TEST_P(PaintPreviewCompositorBeginCompositeTest, MissingSubFrameRecording) {
309   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
310   gfx::Size root_frame_scroll_extent(100, 200);
311   const base::UnguessableToken kSubframe_0_ID =
312       base::UnguessableToken::Create();
313   gfx::Size subframe_0_scroll_extent(50, 75);
314   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
315   const base::UnguessableToken kSubframe_0_0_ID =
316       base::UnguessableToken::Create();
317   gfx::Size subframe_0_0_scroll_extent(20, 20);
318   gfx::Rect subframe_0_0_clip_rect(10, 10, 20, 20);
319   const base::UnguessableToken kSubframe_0_1_ID =
320       base::UnguessableToken::Create();
321   gfx::Size subframe_0_1_scroll_extent(10, 5);
322   gfx::Rect subframe_0_1_clip_rect(10, 10, 30, 30);
323   const base::UnguessableToken kSubframe_1_ID =
324       base::UnguessableToken::Create();
325   gfx::Size subframe_1_scroll_extent(1, 1);
326   gfx::Rect subframe_1_clip_rect(0, 0, 1, 1);
327 
328   PaintPreviewProto proto;
329   proto.mutable_metadata()->set_url(url_.spec());
330   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
331   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
332                      temp_dir_.GetPath().AppendASCII("root.skp"),
333                      root_frame_scroll_extent,
334                      {{kSubframe_0_ID, subframe_0_clip_rect},
335                       {kSubframe_1_ID, subframe_1_clip_rect}},
336                      &expected_data);
337   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
338                      temp_dir_.GetPath().AppendASCII("subframe_0.skp"),
339                      subframe_0_scroll_extent,
340                      {{kSubframe_0_0_ID, subframe_0_0_clip_rect},
341                       {kSubframe_0_1_ID, subframe_0_1_clip_rect}},
342                      &expected_data);
343   PopulateFrameProto(proto.add_subframes(), kSubframe_0_0_ID, false,
344                      temp_dir_.GetPath().AppendASCII("subframe_0_0.skp"),
345                      subframe_0_0_scroll_extent, {}, &expected_data);
346   PopulateFrameProto(proto.add_subframes(), kSubframe_0_1_ID, false,
347                      temp_dir_.GetPath().AppendASCII("subframe_0_1.skp"),
348                      subframe_0_1_scroll_extent, {}, &expected_data);
349   PopulateFrameProto(proto.add_subframes(), kSubframe_1_ID, false,
350                      temp_dir_.GetPath().AppendASCII("subframe_1.skp"),
351                      subframe_1_scroll_extent, {}, &expected_data);
352   auto recording_map = RecordingMapFromPaintPreviewProto(proto);
353   // Missing a subframe SKP is still valid. Compositing will ignore it in the
354   // results.
355   recording_map.erase(kSubframe_0_0_ID);
356   expected_data.erase(kSubframe_0_0_ID);
357   // Remove the kSubframe_0_0_ID from the subframe list since it isn't
358   // available.
359   auto& vec = expected_data[kSubframe_0_ID]->subframes;
360   vec.front() = std::move(vec.back());
361   vec.pop_back();
362 
363   mojom::PaintPreviewBeginCompositeRequestPtr request =
364       mojom::PaintPreviewBeginCompositeRequest::New();
365   request->recording_map = std::move(recording_map);
366   request->proto = ToReadOnlySharedMemory(proto);
367 
368   BeginCompositeAndValidate(
369       std::move(request),
370       mojom::PaintPreviewCompositor::BeginCompositeStatus::kPartialSuccess,
371       kRootFrameID, std::move(expected_data));
372 }
373 
374 // Ensure that depending on a frame multiple times works.
TEST_P(PaintPreviewCompositorBeginCompositeTest,DuplicateFrame)375 TEST_P(PaintPreviewCompositorBeginCompositeTest, DuplicateFrame) {
376   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
377   gfx::Size root_frame_scroll_extent(100, 200);
378   const base::UnguessableToken kSubframe_0_ID =
379       base::UnguessableToken::Create();
380   gfx::Size subframe_0_scroll_extent(50, 75);
381   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
382 
383   PaintPreviewProto proto;
384   proto.mutable_metadata()->set_url(url_.spec());
385   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
386   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
387                      temp_dir_.GetPath().AppendASCII("root.skp"),
388                      root_frame_scroll_extent,
389                      {{kSubframe_0_ID, subframe_0_clip_rect},
390                       {kSubframe_0_ID, subframe_0_clip_rect}},
391                      &expected_data);
392   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
393                      temp_dir_.GetPath().AppendASCII("subframe_0.skp"),
394                      subframe_0_scroll_extent, {}, &expected_data);
395 
396   mojom::PaintPreviewBeginCompositeRequestPtr request =
397       mojom::PaintPreviewBeginCompositeRequest::New();
398   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
399   request->proto = ToReadOnlySharedMemory(proto);
400 
401   BeginCompositeAndValidate(
402       std::move(request),
403       mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess,
404       kRootFrameID, std::move(expected_data));
405 }
406 
407 // Ensure that a loop in frames works.
TEST_P(PaintPreviewCompositorBeginCompositeTest,FrameDependencyLoop)408 TEST_P(PaintPreviewCompositorBeginCompositeTest, FrameDependencyLoop) {
409   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
410   gfx::Size root_frame_scroll_extent(100, 200);
411   const base::UnguessableToken kSubframe_0_ID =
412       base::UnguessableToken::Create();
413   gfx::Size subframe_0_scroll_extent(50, 75);
414   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
415 
416   PaintPreviewProto proto;
417   proto.mutable_metadata()->set_url(url_.spec());
418   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
419   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
420                      temp_dir_.GetPath().AppendASCII("root.skp"),
421                      root_frame_scroll_extent,
422                      {{kSubframe_0_ID, subframe_0_clip_rect}}, &expected_data);
423   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
424                      temp_dir_.GetPath().AppendASCII("subframe_0.skp"),
425                      subframe_0_scroll_extent,
426                      {{kRootFrameID, subframe_0_clip_rect}}, &expected_data);
427 
428   mojom::PaintPreviewBeginCompositeRequestPtr request =
429       mojom::PaintPreviewBeginCompositeRequest::New();
430   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
431   request->proto = ToReadOnlySharedMemory(proto);
432 
433   BeginCompositeAndValidate(
434       std::move(request),
435       GetParam() == CompositeType::kSeparateFrame
436           ? mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess
437           // In the root frame case, subframes will load until the first cycle.
438           : mojom::PaintPreviewCompositor::BeginCompositeStatus::
439                 kPartialSuccess,
440       kRootFrameID, std::move(expected_data));
441 }
442 
443 // Ensure that a frame referencing itself works correctly.
TEST_P(PaintPreviewCompositorBeginCompositeTest,SelfReference)444 TEST_P(PaintPreviewCompositorBeginCompositeTest, SelfReference) {
445   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
446   gfx::Size root_frame_scroll_extent(100, 200);
447   gfx::Rect root_frame_clip_rect(10, 20, 30, 40);
448 
449   PaintPreviewProto proto;
450   proto.mutable_metadata()->set_url(url_.spec());
451   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
452   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
453                      temp_dir_.GetPath().AppendASCII("root.skp"),
454                      root_frame_scroll_extent,
455                      {{kRootFrameID, root_frame_clip_rect}}, &expected_data);
456 
457   mojom::PaintPreviewBeginCompositeRequestPtr request =
458       mojom::PaintPreviewBeginCompositeRequest::New();
459   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
460   request->proto = ToReadOnlySharedMemory(proto);
461 
462   BeginCompositeAndValidate(
463       std::move(request),
464       GetParam() == CompositeType::kSeparateFrame
465           ? mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess
466           // In the root frame case, a frame cannot be embedded in itself.
467           : mojom::PaintPreviewCompositor::BeginCompositeStatus::
468                 kPartialSuccess,
469       kRootFrameID, std::move(expected_data));
470 }
471 
TEST_P(PaintPreviewCompositorBeginCompositeTest,InvalidRegionHandling)472 TEST_P(PaintPreviewCompositorBeginCompositeTest, InvalidRegionHandling) {
473   mojom::PaintPreviewBeginCompositeRequestPtr request =
474       mojom::PaintPreviewBeginCompositeRequest::New();
475 
476   BeginCompositeAndValidate(
477       std::move(request),
478       mojom::PaintPreviewCompositor::BeginCompositeStatus::
479           kDeserializingFailure,
480       base::UnguessableToken::Create(),
481       base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>());
482 }
483 
TEST_P(PaintPreviewCompositorBeginCompositeTest,InvalidProto)484 TEST_P(PaintPreviewCompositorBeginCompositeTest, InvalidProto) {
485   mojom::PaintPreviewBeginCompositeRequestPtr request =
486       mojom::PaintPreviewBeginCompositeRequest::New();
487   std::string test_data = "hello world";
488   auto region = base::WritableSharedMemoryRegion::Create(test_data.size());
489   ASSERT_TRUE(region.IsValid());
490   auto mapping = region.Map();
491   ASSERT_TRUE(mapping.IsValid());
492   memcpy(mapping.memory(), test_data.data(), mapping.size());
493 
494   // These calls log errors without a newline (from the proto lib). As a
495   // result, the Android gtest parser fails to parse the test status. To work
496   // around this gobble the log message.
497   {
498     testing::internal::CaptureStdout();
499     request->proto =
500         base::WritableSharedMemoryRegion::ConvertToReadOnly(std::move(region));
501     BeginCompositeAndValidate(
502         std::move(request),
503         mojom::PaintPreviewCompositor::BeginCompositeStatus::
504             kDeserializingFailure,
505         base::UnguessableToken::Create(),
506         base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>());
507     LOG(ERROR) << testing::internal::GetCapturedStdout();
508   }
509 }
510 
TEST_P(PaintPreviewCompositorBeginCompositeTest,InvalidRootFrame)511 TEST_P(PaintPreviewCompositorBeginCompositeTest, InvalidRootFrame) {
512   mojom::PaintPreviewBeginCompositeRequestPtr request =
513       mojom::PaintPreviewBeginCompositeRequest::New();
514   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
515   PaintPreviewProto proto;
516   proto.mutable_metadata()->set_url(url_.spec());
517   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
518   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
519                      temp_dir_.GetPath().AppendASCII("root.skp"),
520                      gfx::Size(1, 1), {}, &expected_data);
521   auto recording_map = RecordingMapFromPaintPreviewProto(proto);
522   recording_map.erase(
523       kRootFrameID);  // Missing a SKP for the root file is invalid.
524   request->recording_map = std::move(recording_map);
525   request->proto = ToReadOnlySharedMemory(proto);
526   BeginCompositeAndValidate(
527       std::move(request),
528       mojom::PaintPreviewCompositor::BeginCompositeStatus::kCompositingFailure,
529       base::UnguessableToken::Create(),
530       base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>());
531 }
532 
533 // Ensure that scroll offsets are correctly returned in the
534 // |BeginSeparatedFrameComposite| case.
TEST_P(PaintPreviewCompositorBeginCompositeTest,SubframeWithScrollOffsets)535 TEST_P(PaintPreviewCompositorBeginCompositeTest, SubframeWithScrollOffsets) {
536   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
537   gfx::Size root_frame_scroll_extent(100, 200);
538   const base::UnguessableToken kSubframe_0_ID =
539       base::UnguessableToken::Create();
540   gfx::Size subframe_0_scroll_extent(50, 75);
541   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
542   gfx::Size subframe_0_scroll_offsets(34, 56);
543 
544   PaintPreviewProto proto;
545   proto.mutable_metadata()->set_url(url_.spec());
546   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
547   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
548                      temp_dir_.GetPath().AppendASCII("root.skp"),
549                      root_frame_scroll_extent,
550                      {{kSubframe_0_ID, subframe_0_clip_rect}}, &expected_data);
551   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
552                      temp_dir_.GetPath().AppendASCII("subframe_0.skp"),
553                      subframe_0_scroll_extent, {}, &expected_data,
554                      subframe_0_scroll_offsets);
555 
556   mojom::PaintPreviewBeginCompositeRequestPtr request =
557       mojom::PaintPreviewBeginCompositeRequest::New();
558   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
559   request->proto = ToReadOnlySharedMemory(proto);
560 
561   BeginCompositeAndValidate(
562       std::move(request),
563       mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess,
564       kRootFrameID, std::move(expected_data));
565 }
566 
567 INSTANTIATE_TEST_SUITE_P(All,
568                          PaintPreviewCompositorBeginCompositeTest,
569                          testing::Values(CompositeType::kSeparateFrame,
570                                          CompositeType::kMainFrame),
571                          CompositeTypeParamToString);
572 
TEST(PaintPreviewCompositorTest,TestComposite)573 TEST(PaintPreviewCompositorTest, TestComposite) {
574   base::test::TaskEnvironment task_environment;
575   base::ScopedTempDir temp_dir;
576   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
577   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
578                                         base::BindOnce([]() {}));
579   GURL url("https://www.chromium.org");
580   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
581   gfx::Size root_frame_scroll_extent(100, 200);
582   PaintPreviewProto proto;
583   proto.mutable_metadata()->set_url(url.spec());
584   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
585   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
586                      temp_dir.GetPath().AppendASCII("root.skp"),
587                      root_frame_scroll_extent, {}, &expected_data);
588   mojom::PaintPreviewBeginCompositeRequestPtr request =
589       mojom::PaintPreviewBeginCompositeRequest::New();
590   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
591   request->proto = ToReadOnlySharedMemory(proto);
592   compositor.BeginSeparatedFrameComposite(
593       std::move(request),
594       base::BindOnce(
595           &BeginSeparatedFrameCompositeCallbackImpl,
596           mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess,
597           kRootFrameID, std::move(expected_data)));
598   float scale_factor = 2;
599   gfx::Rect rect = gfx::ScaleToEnclosingRect(
600       gfx::Rect(root_frame_scroll_extent), scale_factor);
601   SkBitmap bitmap;
602   bitmap.allocPixels(
603       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
604   SkCanvas canvas(bitmap, SkSurfaceProps{});
605   canvas.scale(scale_factor, scale_factor);
606   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
607   compositor.BitmapForSeparatedFrame(
608       kRootFrameID, rect, scale_factor,
609       base::BindOnce(&BitmapCallbackImpl,
610                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
611                      bitmap));
612   task_environment.RunUntilIdle();
613   compositor.BitmapForSeparatedFrame(
614       base::UnguessableToken::Create(), rect, scale_factor,
615       base::BindOnce(&BitmapCallbackImpl,
616                      mojom::PaintPreviewCompositor::BitmapStatus::kMissingFrame,
617                      bitmap));
618   task_environment.RunUntilIdle();
619 }
620 
TEST(PaintPreviewCompositorTest,TestCompositeWithMemoryBuffer)621 TEST(PaintPreviewCompositorTest, TestCompositeWithMemoryBuffer) {
622   base::test::TaskEnvironment task_environment;
623   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
624                                         base::BindOnce([]() {}));
625   GURL url("https://www.chromium.org");
626   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
627   gfx::Size root_frame_scroll_extent(100, 200);
628   PaintPreviewProto proto;
629   proto.mutable_metadata()->set_url(url.spec());
630 
631   PaintPreviewFrameProto* root_frame = proto.mutable_root_frame();
632   root_frame->set_embedding_token_low(kRootFrameID.GetLowForSerialization());
633   root_frame->set_embedding_token_high(kRootFrameID.GetHighForSerialization());
634   root_frame->set_is_main_frame(true);
635 
636   mojo_base::BigBuffer buffer;
637   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
638   {
639     SkPictureRecorder recorder;
640     SkCanvas* canvas =
641         recorder.beginRecording(ToSkRect(root_frame_scroll_extent));
642     DrawDummyTestPicture(canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
643     sk_sp<SkPicture> pic = recorder.finishRecordingAsPicture();
644 
645     PaintPreviewTracker tracker(base::UnguessableToken::Create(), kRootFrameID,
646                                 /*is_main_frame=*/true);
647     size_t serialized_size = 0;
648     auto result =
649         RecordToBuffer(pic, &tracker, base::nullopt, &serialized_size);
650     ASSERT_TRUE(result.has_value());
651     buffer = std::move(result.value());
652 
653     mojom::FrameDataPtr frame_data = mojom::FrameData::New();
654     frame_data->scroll_extents = root_frame_scroll_extent;
655     expected_data.insert({kRootFrameID, std::move(frame_data)});
656   }
657 
658   mojom::PaintPreviewBeginCompositeRequestPtr request =
659       mojom::PaintPreviewBeginCompositeRequest::New();
660   request->recording_map.insert(
661       {kRootFrameID, SerializedRecording(std::move(buffer))});
662   request->proto = ToReadOnlySharedMemory(proto);
663 
664   compositor.BeginSeparatedFrameComposite(
665       std::move(request),
666       base::BindOnce(
667           &BeginSeparatedFrameCompositeCallbackImpl,
668           mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess,
669           kRootFrameID, std::move(expected_data)));
670   float scale_factor = 2;
671   gfx::Rect rect = gfx::ScaleToEnclosingRect(
672       gfx::Rect(root_frame_scroll_extent), scale_factor);
673   SkBitmap bitmap;
674   bitmap.allocPixels(
675       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
676   SkCanvas canvas(bitmap, SkSurfaceProps{});
677   canvas.scale(scale_factor, scale_factor);
678   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
679   compositor.BitmapForSeparatedFrame(
680       kRootFrameID, rect, scale_factor,
681       base::BindOnce(&BitmapCallbackImpl,
682                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
683                      bitmap));
684   task_environment.RunUntilIdle();
685   compositor.BitmapForSeparatedFrame(
686       base::UnguessableToken::Create(), rect, scale_factor,
687       base::BindOnce(&BitmapCallbackImpl,
688                      mojom::PaintPreviewCompositor::BitmapStatus::kMissingFrame,
689                      bitmap));
690   task_environment.RunUntilIdle();
691 }
692 
TEST(PaintPreviewCompositorTest,TestCompositeMainFrameNoDependencies)693 TEST(PaintPreviewCompositorTest, TestCompositeMainFrameNoDependencies) {
694   base::test::TaskEnvironment task_environment;
695   base::ScopedTempDir temp_dir;
696   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
697   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
698                                         base::BindOnce([]() {}));
699   GURL url("https://www.chromium.org");
700   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
701   gfx::Size root_frame_scroll_extent(100, 200);
702   PaintPreviewProto proto;
703   proto.mutable_metadata()->set_url(url.spec());
704   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
705   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
706                      temp_dir.GetPath().AppendASCII("root.skp"),
707                      root_frame_scroll_extent, {}, &expected_data);
708   mojom::PaintPreviewBeginCompositeRequestPtr request =
709       mojom::PaintPreviewBeginCompositeRequest::New();
710   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
711   request->proto = ToReadOnlySharedMemory(proto);
712   compositor.BeginMainFrameComposite(
713       std::move(request),
714       base::BindOnce(
715           [](mojom::PaintPreviewCompositor::BeginCompositeStatus status) {
716             EXPECT_EQ(
717                 status,
718                 mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess);
719           }));
720   float scale_factor = 2;
721   gfx::Rect rect = gfx::ScaleToEnclosingRect(
722       gfx::Rect(root_frame_scroll_extent), scale_factor);
723   SkBitmap bitmap;
724   bitmap.allocPixels(
725       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
726   SkCanvas canvas(bitmap, SkSurfaceProps{});
727   canvas.scale(scale_factor, scale_factor);
728   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
729   compositor.BitmapForMainFrame(
730       rect, scale_factor,
731       base::BindOnce(&BitmapCallbackImpl,
732                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
733                      bitmap));
734   task_environment.RunUntilIdle();
735 }
736 
TEST(PaintPreviewCompositorTest,TestCompositeMainFrameOneDependency)737 TEST(PaintPreviewCompositorTest, TestCompositeMainFrameOneDependency) {
738   base::test::TaskEnvironment task_environment;
739   base::ScopedTempDir temp_dir;
740   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
741   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
742                                         base::BindOnce([]() {}));
743   GURL url("https://www.chromium.org");
744   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
745   gfx::Size root_frame_scroll_extent(100, 200);
746   const base::UnguessableToken kSubframe_0_ID =
747       base::UnguessableToken::Create();
748   gfx::Size subframe_0_scroll_extent(50, 75);
749   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
750 
751   PaintPreviewProto proto;
752   proto.mutable_metadata()->set_url(url.spec());
753   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
754   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
755                      temp_dir.GetPath().AppendASCII("root.skp"),
756                      root_frame_scroll_extent,
757                      {{kSubframe_0_ID, subframe_0_clip_rect}}, &expected_data);
758   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
759                      temp_dir.GetPath().AppendASCII("subframe_0.skp"),
760                      subframe_0_scroll_extent, {}, &expected_data, gfx::Size(),
761                      SK_ColorLTGRAY);
762 
763   mojom::PaintPreviewBeginCompositeRequestPtr request =
764       mojom::PaintPreviewBeginCompositeRequest::New();
765   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
766   request->proto = ToReadOnlySharedMemory(proto);
767   compositor.BeginMainFrameComposite(
768       std::move(request),
769       base::BindOnce(
770           [](mojom::PaintPreviewCompositor::BeginCompositeStatus status) {
771             EXPECT_EQ(
772                 status,
773                 mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess);
774           }));
775   float scale_factor = 1;
776   gfx::Rect rect = gfx::ScaleToEnclosingRect(
777       gfx::Rect(root_frame_scroll_extent), scale_factor);
778   SkBitmap bitmap;
779   bitmap.allocPixels(
780       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
781   SkCanvas canvas(bitmap, SkSurfaceProps{});
782   canvas.scale(scale_factor, scale_factor);
783   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
784   // Draw the subframe where we embedded it while populating the proto.
785   DrawDummyTestPicture(&canvas, SK_ColorLTGRAY, subframe_0_scroll_extent,
786                        subframe_0_clip_rect);
787   compositor.BitmapForMainFrame(
788       rect, scale_factor,
789       base::BindOnce(&BitmapCallbackImpl,
790                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
791                      bitmap));
792   task_environment.RunUntilIdle();
793 }
794 
TEST(PaintPreviewCompositorTest,TestCompositeMainFrameOneDependencyScrolled)795 TEST(PaintPreviewCompositorTest, TestCompositeMainFrameOneDependencyScrolled) {
796   base::test::TaskEnvironment task_environment;
797   base::ScopedTempDir temp_dir;
798   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
799   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
800                                         base::BindOnce([]() {}));
801   GURL url("https://www.chromium.org");
802   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
803   gfx::Size root_frame_scroll_extent(100, 200);
804   const base::UnguessableToken kSubframe_0_ID =
805       base::UnguessableToken::Create();
806   gfx::Size subframe_0_scroll_extent(50, 75);
807   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
808   gfx::Size subframe_0_scroll_offsets(0, 5);
809 
810   PaintPreviewProto proto;
811   proto.mutable_metadata()->set_url(url.spec());
812   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
813   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
814                      temp_dir.GetPath().AppendASCII("root.skp"),
815                      root_frame_scroll_extent,
816                      {{kSubframe_0_ID, subframe_0_clip_rect}}, &expected_data);
817   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
818                      temp_dir.GetPath().AppendASCII("subframe_0.skp"),
819                      subframe_0_scroll_extent, {}, &expected_data,
820                      subframe_0_scroll_offsets, SK_ColorLTGRAY);
821 
822   mojom::PaintPreviewBeginCompositeRequestPtr request =
823       mojom::PaintPreviewBeginCompositeRequest::New();
824   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
825   request->proto = ToReadOnlySharedMemory(proto);
826   compositor.BeginMainFrameComposite(
827       std::move(request),
828       base::BindOnce(
829           [](mojom::PaintPreviewCompositor::BeginCompositeStatus status) {
830             EXPECT_EQ(
831                 status,
832                 mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess);
833           }));
834   float scale_factor = 1;
835   gfx::Rect rect = gfx::ScaleToEnclosingRect(
836       gfx::Rect(root_frame_scroll_extent), scale_factor);
837   SkBitmap bitmap;
838   bitmap.allocPixels(
839       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
840   SkCanvas canvas(bitmap, SkSurfaceProps{});
841   canvas.scale(scale_factor, scale_factor);
842   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent);
843   // Draw the subframe where we embedded it while populating the proto.
844   DrawDummyTestPicture(&canvas, SK_ColorLTGRAY, subframe_0_scroll_extent,
845                        subframe_0_clip_rect, subframe_0_scroll_offsets);
846   compositor.BitmapForMainFrame(
847       rect, scale_factor,
848       base::BindOnce(&BitmapCallbackImpl,
849                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
850                      bitmap));
851   task_environment.RunUntilIdle();
852 }
853 
TEST(PaintPreviewCompositorTest,TestCompositeMainFrameOneDependencyWithRootFrameScrolled)854 TEST(PaintPreviewCompositorTest,
855      TestCompositeMainFrameOneDependencyWithRootFrameScrolled) {
856   base::test::TaskEnvironment task_environment;
857   base::ScopedTempDir temp_dir;
858   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
859   PaintPreviewCompositorImpl compositor(mojo::NullReceiver(), nullptr,
860                                         base::BindOnce([]() {}));
861   GURL url("https://www.chromium.org");
862   const base::UnguessableToken kRootFrameID = base::UnguessableToken::Create();
863   gfx::Size root_frame_scroll_extent(110, 215);
864   gfx::Size root_frame_scroll_offsets(10, 15);
865   gfx::Rect root_frame_clip_rect(10, 15, 100, 200);
866   const base::UnguessableToken kSubframe_0_ID =
867       base::UnguessableToken::Create();
868   gfx::Size subframe_0_scroll_extent(50, 75);
869   gfx::Rect subframe_0_clip_rect(10, 20, 30, 40);
870 
871   PaintPreviewProto proto;
872   proto.mutable_metadata()->set_url(url.spec());
873   base::flat_map<base::UnguessableToken, mojom::FrameDataPtr> expected_data;
874   PopulateFrameProto(proto.mutable_root_frame(), kRootFrameID, true,
875                      temp_dir.GetPath().AppendASCII("root.skp"),
876                      root_frame_scroll_extent,
877                      {{kSubframe_0_ID, subframe_0_clip_rect}}, &expected_data,
878                      root_frame_scroll_offsets);
879   PopulateFrameProto(proto.add_subframes(), kSubframe_0_ID, false,
880                      temp_dir.GetPath().AppendASCII("subframe_0.skp"),
881                      subframe_0_scroll_extent, {}, &expected_data, gfx::Size(),
882                      SK_ColorLTGRAY);
883 
884   mojom::PaintPreviewBeginCompositeRequestPtr request =
885       mojom::PaintPreviewBeginCompositeRequest::New();
886   request->recording_map = RecordingMapFromPaintPreviewProto(proto);
887   request->proto = ToReadOnlySharedMemory(proto);
888   compositor.BeginMainFrameComposite(
889       std::move(request),
890       base::BindOnce(
891           [](mojom::PaintPreviewCompositor::BeginCompositeStatus status) {
892             EXPECT_EQ(
893                 status,
894                 mojom::PaintPreviewCompositor::BeginCompositeStatus::kSuccess);
895           }));
896   float scale_factor = 1;
897   gfx::Rect rect =
898       gfx::ScaleToEnclosingRect(root_frame_clip_rect, scale_factor);
899   SkBitmap bitmap;
900   bitmap.allocPixels(
901       SkImageInfo::MakeN32(rect.width(), rect.height(), kOpaque_SkAlphaType));
902   SkCanvas canvas(bitmap, SkSurfaceProps{});
903   canvas.scale(scale_factor, scale_factor);
904   // Offset the canvas to simulate the root frame being scrolled.
905   canvas.translate(-root_frame_clip_rect.x(), -root_frame_clip_rect.y());
906   DrawDummyTestPicture(&canvas, SK_ColorDKGRAY, root_frame_scroll_extent,
907                        root_frame_clip_rect, root_frame_scroll_offsets);
908   DrawDummyTestPicture(&canvas, SK_ColorLTGRAY, subframe_0_scroll_extent,
909                        subframe_0_clip_rect);
910   compositor.BitmapForMainFrame(
911       rect, scale_factor,
912       base::BindOnce(&BitmapCallbackImpl,
913                      mojom::PaintPreviewCompositor::BitmapStatus::kSuccess,
914                      bitmap));
915   task_environment.RunUntilIdle();
916 }
917 
918 }  // namespace paint_preview
919