extension_icon_image_unittest.cc revision 116680a4aac90f2aa7413d9095a592090648e557
1// Copyright 2014 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 "extensions/browser/extension_icon_image.h"
6
7#include "base/json/json_file_value_serializer.h"
8#include "base/message_loop/message_loop.h"
9#include "base/path_service.h"
10#include "chrome/common/chrome_paths.h"
11#include "chrome/test/base/testing_profile.h"
12#include "content/public/test/test_browser_thread.h"
13#include "extensions/browser/image_loader.h"
14#include "extensions/common/extension.h"
15#include "extensions/common/manifest.h"
16#include "extensions/common/manifest_handlers/icons_handler.h"
17#include "skia/ext/image_operations.h"
18#include "testing/gtest/include/gtest/gtest.h"
19#include "ui/base/resource/resource_bundle.h"
20#include "ui/gfx/image/image_skia_source.h"
21#include "ui/gfx/skia_util.h"
22
23using content::BrowserThread;
24using extensions::Extension;
25using extensions::IconImage;
26using extensions::Manifest;
27
28namespace {
29
30SkBitmap CreateBlankBitmapForScale(int size_dip, ui::ScaleFactor scale_factor) {
31  SkBitmap bitmap;
32  const float scale = ui::GetScaleForScaleFactor(scale_factor);
33  bitmap.allocN32Pixels(static_cast<int>(size_dip * scale),
34                        static_cast<int>(size_dip * scale));
35  bitmap.eraseColor(SkColorSetARGB(0, 0, 0, 0));
36  return bitmap;
37}
38
39SkBitmap EnsureBitmapSize(const SkBitmap& original, int size) {
40  if (original.width() == size && original.height() == size)
41    return original;
42
43  SkBitmap resized = skia::ImageOperations::Resize(
44      original, skia::ImageOperations::RESIZE_LANCZOS3, size, size);
45  return resized;
46}
47
48// Used to test behavior including images defined by an image skia source.
49// |GetImageForScale| simply returns image representation from the image given
50// in the ctor.
51class MockImageSkiaSource : public gfx::ImageSkiaSource {
52 public:
53  explicit MockImageSkiaSource(const gfx::ImageSkia& image)
54      : image_(image) {
55  }
56  virtual ~MockImageSkiaSource() {}
57
58  virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE {
59    return image_.GetRepresentation(scale);
60  }
61
62 private:
63  gfx::ImageSkia image_;
64};
65
66// Helper class for synchronously loading extension image resource.
67class TestImageLoader {
68 public:
69  explicit TestImageLoader(const Extension* extension)
70      : extension_(extension),
71        waiting_(false),
72        image_loaded_(false) {
73  }
74  virtual ~TestImageLoader() {}
75
76  void OnImageLoaded(const gfx::Image& image) {
77    image_ = image;
78    image_loaded_ = true;
79    if (waiting_)
80      base::MessageLoop::current()->Quit();
81  }
82
83  SkBitmap LoadBitmap(const std::string& path,
84                      int size) {
85    image_loaded_ = false;
86
87    image_loader_.LoadImageAsync(
88        extension_, extension_->GetResource(path), gfx::Size(size, size),
89        base::Bind(&TestImageLoader::OnImageLoaded,
90                   base::Unretained(this)));
91
92    // If |image_| still hasn't been loaded (i.e. it is being loaded
93    // asynchronously), wait for it.
94    if (!image_loaded_) {
95      waiting_ = true;
96      base::MessageLoop::current()->Run();
97      waiting_ = false;
98    }
99
100    EXPECT_TRUE(image_loaded_);
101
102    return image_.IsEmpty() ? SkBitmap() : *image_.ToSkBitmap();
103  }
104
105 private:
106  const Extension* extension_;
107  bool waiting_;
108  bool image_loaded_;
109  gfx::Image image_;
110  extensions::ImageLoader image_loader_;
111
112  DISALLOW_COPY_AND_ASSIGN(TestImageLoader);
113};
114
115class ExtensionIconImageTest : public testing::Test,
116                               public IconImage::Observer {
117 public:
118  ExtensionIconImageTest()
119      : image_loaded_count_(0),
120        quit_in_image_loaded_(false),
121        ui_thread_(BrowserThread::UI, &ui_loop_),
122        file_thread_(BrowserThread::FILE),
123        io_thread_(BrowserThread::IO) {
124  }
125
126  virtual ~ExtensionIconImageTest() {}
127
128  void WaitForImageLoad() {
129    quit_in_image_loaded_ = true;
130    base::MessageLoop::current()->Run();
131    quit_in_image_loaded_ = false;
132  }
133
134  int ImageLoadedCount() {
135    int result = image_loaded_count_;
136    image_loaded_count_ = 0;
137    return result;
138  }
139
140  scoped_refptr<Extension> CreateExtension(const char* name,
141                                           Manifest::Location location) {
142    // Create and load an extension.
143    base::FilePath test_file;
144    if (!PathService::Get(chrome::DIR_TEST_DATA, &test_file)) {
145      EXPECT_FALSE(true);
146      return NULL;
147    }
148    test_file = test_file.AppendASCII("extensions").AppendASCII(name);
149    int error_code = 0;
150    std::string error;
151    JSONFileValueSerializer serializer(test_file.AppendASCII("app.json"));
152    scoped_ptr<base::DictionaryValue> valid_value(
153        static_cast<base::DictionaryValue*>(serializer.Deserialize(&error_code,
154                                                                   &error)));
155    EXPECT_EQ(0, error_code) << error;
156    if (error_code != 0)
157      return NULL;
158
159    EXPECT_TRUE(valid_value.get());
160    if (!valid_value)
161      return NULL;
162
163    return Extension::Create(test_file, location, *valid_value,
164                             Extension::NO_FLAGS, &error);
165  }
166
167  // testing::Test overrides:
168  virtual void SetUp() OVERRIDE {
169    file_thread_.Start();
170    io_thread_.Start();
171  }
172
173  // IconImage::Delegate overrides:
174  virtual void OnExtensionIconImageChanged(IconImage* image) OVERRIDE {
175    image_loaded_count_++;
176    if (quit_in_image_loaded_)
177      base::MessageLoop::current()->Quit();
178  }
179
180  gfx::ImageSkia GetDefaultIcon() {
181    return gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(16, 16), 1.0f));
182  }
183
184  // Loads an image to be used in test from the extension.
185  // The image will be loaded from the relative path |path|.
186  SkBitmap GetTestBitmap(const Extension* extension,
187                         const std::string& path,
188                         int size) {
189    TestImageLoader image_loader(extension);
190    return image_loader.LoadBitmap(path, size);
191  }
192
193 private:
194  int image_loaded_count_;
195  bool quit_in_image_loaded_;
196  base::MessageLoop ui_loop_;
197  content::TestBrowserThread ui_thread_;
198  content::TestBrowserThread file_thread_;
199  content::TestBrowserThread io_thread_;
200
201  DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest);
202};
203
204}  // namespace
205
206TEST_F(ExtensionIconImageTest, Basic) {
207  std::vector<ui::ScaleFactor> supported_factors;
208  supported_factors.push_back(ui::SCALE_FACTOR_100P);
209  supported_factors.push_back(ui::SCALE_FACTOR_200P);
210  ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors);
211  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
212  scoped_refptr<Extension> extension(CreateExtension(
213      "extension_icon_image", Manifest::INVALID_LOCATION));
214  ASSERT_TRUE(extension.get() != NULL);
215
216  gfx::ImageSkia default_icon = GetDefaultIcon();
217
218  // Load images we expect to find as representations in icon_image, so we
219  // can later use them to validate icon_image.
220  SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16);
221  ASSERT_FALSE(bitmap_16.empty());
222
223  // There is no image of size 32 defined in the extension manifest, so we
224  // should expect manifest image of size 48 resized to size 32.
225  SkBitmap bitmap_48_resized_to_32 =
226      GetTestBitmap(extension.get(), "48.png", 32);
227  ASSERT_FALSE(bitmap_48_resized_to_32.empty());
228
229  IconImage image(profile.get(),
230                  extension.get(),
231                  extensions::IconsInfo::GetIcons(extension.get()),
232                  16,
233                  default_icon,
234                  this);
235
236  // No representations in |image_| yet.
237  gfx::ImageSkia::ImageSkiaReps image_reps = image.image_skia().image_reps();
238  ASSERT_EQ(0u, image_reps.size());
239
240  // Gets representation for a scale factor.
241  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
242
243  // Before the image representation is loaded, image should contain blank
244  // image representation.
245  EXPECT_TRUE(gfx::BitmapsAreEqual(
246      representation.sk_bitmap(),
247      CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_100P)));
248
249  WaitForImageLoad();
250  EXPECT_EQ(1, ImageLoadedCount());
251  ASSERT_EQ(1u, image.image_skia().image_reps().size());
252
253  representation = image.image_skia().GetRepresentation(1.0f);
254
255  // We should get the right representation now.
256  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16));
257  EXPECT_EQ(16, representation.pixel_width());
258
259  // Gets representation for an additional scale factor.
260  representation = image.image_skia().GetRepresentation(2.0f);
261
262  EXPECT_TRUE(gfx::BitmapsAreEqual(
263      representation.sk_bitmap(),
264      CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P)));
265
266  WaitForImageLoad();
267  EXPECT_EQ(1, ImageLoadedCount());
268  ASSERT_EQ(2u, image.image_skia().image_reps().size());
269
270  representation = image.image_skia().GetRepresentation(2.0f);
271
272  // Image should have been resized.
273  EXPECT_EQ(32, representation.pixel_width());
274  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
275                                   bitmap_48_resized_to_32));
276}
277
278// There is no resource with either exact or bigger size, but there is a smaller
279// resource.
280TEST_F(ExtensionIconImageTest, FallbackToSmallerWhenNoBigger) {
281  std::vector<ui::ScaleFactor> supported_factors;
282  supported_factors.push_back(ui::SCALE_FACTOR_100P);
283  supported_factors.push_back(ui::SCALE_FACTOR_200P);
284  ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors);
285  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
286  scoped_refptr<Extension> extension(CreateExtension(
287      "extension_icon_image", Manifest::INVALID_LOCATION));
288  ASSERT_TRUE(extension.get() != NULL);
289
290  gfx::ImageSkia default_icon = GetDefaultIcon();
291
292  // Load images we expect to find as representations in icon_image, so we
293  // can later use them to validate icon_image.
294  SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48);
295  ASSERT_FALSE(bitmap_48.empty());
296
297  IconImage image(profile.get(),
298                  extension.get(),
299                  extensions::IconsInfo::GetIcons(extension.get()),
300                  32,
301                  default_icon,
302                  this);
303
304  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(2.0f);
305
306  WaitForImageLoad();
307  EXPECT_EQ(1, ImageLoadedCount());
308  ASSERT_EQ(1u, image.image_skia().image_reps().size());
309
310  representation = image.image_skia().GetRepresentation(2.0f);
311
312  // We should have loaded the biggest smaller resource resized to the actual
313  // size.
314  EXPECT_EQ(2.0f, representation.scale());
315  EXPECT_EQ(64, representation.pixel_width());
316  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
317                                   EnsureBitmapSize(bitmap_48, 64)));
318}
319
320// There is no resource with exact size, but there is a smaller and a bigger
321// one. Requested size is smaller than 32 though, so the smaller resource should
322// be loaded.
323TEST_F(ExtensionIconImageTest, FallbackToSmaller) {
324  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
325  scoped_refptr<Extension> extension(CreateExtension(
326      "extension_icon_image", Manifest::INVALID_LOCATION));
327  ASSERT_TRUE(extension.get() != NULL);
328
329  gfx::ImageSkia default_icon = GetDefaultIcon();
330
331  // Load images we expect to find as representations in icon_image, so we
332  // can later use them to validate icon_image.
333  SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16);
334  ASSERT_FALSE(bitmap_16.empty());
335
336  IconImage image(profile.get(),
337                  extension.get(),
338                  extensions::IconsInfo::GetIcons(extension.get()),
339                  17,
340                  default_icon,
341                  this);
342
343  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
344
345  WaitForImageLoad();
346  EXPECT_EQ(1, ImageLoadedCount());
347  ASSERT_EQ(1u, image.image_skia().image_reps().size());
348
349  representation = image.image_skia().GetRepresentation(1.0f);
350
351  // We should have loaded smaller (resized) resource.
352  EXPECT_EQ(1.0f, representation.scale());
353  EXPECT_EQ(17, representation.pixel_width());
354  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(),
355                                   EnsureBitmapSize(bitmap_16, 17)));
356}
357
358// If resource set is empty, |GetRepresentation| should synchronously return
359// default icon, without notifying observer of image change.
360TEST_F(ExtensionIconImageTest, NoResources) {
361  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
362  scoped_refptr<Extension> extension(CreateExtension(
363      "extension_icon_image", Manifest::INVALID_LOCATION));
364  ASSERT_TRUE(extension.get() != NULL);
365
366  ExtensionIconSet empty_icon_set;
367  gfx::ImageSkia default_icon = GetDefaultIcon();
368
369  const int kRequestedSize = 24;
370  IconImage image(profile.get(),
371                  extension.get(),
372                  empty_icon_set,
373                  kRequestedSize,
374                  default_icon,
375                  this);
376
377  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
378  EXPECT_TRUE(gfx::BitmapsAreEqual(
379      representation.sk_bitmap(),
380      EnsureBitmapSize(
381          default_icon.GetRepresentation(1.0f).sk_bitmap(),
382          kRequestedSize)));
383
384  EXPECT_EQ(0, ImageLoadedCount());
385  // We should have a default icon representation.
386  ASSERT_EQ(1u, image.image_skia().image_reps().size());
387
388  representation = image.image_skia().GetRepresentation(1.0f);
389  EXPECT_TRUE(gfx::BitmapsAreEqual(
390      representation.sk_bitmap(),
391      EnsureBitmapSize(
392          default_icon.GetRepresentation(1.0f).sk_bitmap(),
393          kRequestedSize)));
394}
395
396// If resource set is invalid, image load should be done asynchronously and
397// the observer should be notified when it's done. |GetRepresentation| should
398// return the default icon representation once image load is done.
399TEST_F(ExtensionIconImageTest, InvalidResource) {
400  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
401  scoped_refptr<Extension> extension(CreateExtension(
402      "extension_icon_image", Manifest::INVALID_LOCATION));
403  ASSERT_TRUE(extension.get() != NULL);
404
405  const int kInvalidIconSize = 24;
406  ExtensionIconSet invalid_icon_set;
407  invalid_icon_set.Add(kInvalidIconSize, "invalid.png");
408
409  gfx::ImageSkia default_icon = GetDefaultIcon();
410
411  IconImage image(profile.get(),
412                  extension.get(),
413                  invalid_icon_set,
414                  kInvalidIconSize,
415                  default_icon,
416                  this);
417
418  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
419  EXPECT_TRUE(gfx::BitmapsAreEqual(
420      representation.sk_bitmap(),
421      CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P)));
422
423  WaitForImageLoad();
424  EXPECT_EQ(1, ImageLoadedCount());
425  // We should have default icon representation now.
426  ASSERT_EQ(1u, image.image_skia().image_reps().size());
427
428  representation = image.image_skia().GetRepresentation(1.0f);
429  EXPECT_TRUE(gfx::BitmapsAreEqual(
430      representation.sk_bitmap(),
431      EnsureBitmapSize(
432          default_icon.GetRepresentation(1.0f).sk_bitmap(),
433          kInvalidIconSize)));
434}
435
436// Test that IconImage works with lazily (but synchronously) created default
437// icon when IconImage returns synchronously.
438TEST_F(ExtensionIconImageTest, LazyDefaultIcon) {
439  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
440  scoped_refptr<Extension> extension(CreateExtension(
441      "extension_icon_image", Manifest::INVALID_LOCATION));
442  ASSERT_TRUE(extension.get() != NULL);
443
444  gfx::ImageSkia default_icon = GetDefaultIcon();
445  gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon),
446                                    default_icon.size());
447
448  ExtensionIconSet empty_icon_set;
449
450  const int kRequestedSize = 128;
451  IconImage image(profile.get(),
452                  extension.get(),
453                  empty_icon_set,
454                  kRequestedSize,
455                  lazy_default_icon,
456                  this);
457
458  ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f));
459
460  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
461
462  // The resouce set is empty, so we should get the result right away.
463  EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f));
464  EXPECT_TRUE(gfx::BitmapsAreEqual(
465      representation.sk_bitmap(),
466      EnsureBitmapSize(
467          default_icon.GetRepresentation(1.0f).sk_bitmap(),
468          kRequestedSize)));
469
470  // We should have a default icon representation.
471  ASSERT_EQ(1u, image.image_skia().image_reps().size());
472}
473
474// Test that IconImage works with lazily (but synchronously) created default
475// icon when IconImage returns asynchronously.
476TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) {
477  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
478  scoped_refptr<Extension> extension(CreateExtension(
479      "extension_icon_image", Manifest::INVALID_LOCATION));
480  ASSERT_TRUE(extension.get() != NULL);
481
482  gfx::ImageSkia default_icon = GetDefaultIcon();
483  gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon),
484                                    default_icon.size());
485
486  const int kInvalidIconSize = 24;
487  ExtensionIconSet invalid_icon_set;
488  invalid_icon_set.Add(kInvalidIconSize, "invalid.png");
489
490  IconImage image(profile.get(),
491                  extension.get(),
492                  invalid_icon_set,
493                  kInvalidIconSize,
494                  lazy_default_icon,
495                  this);
496
497  ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f));
498
499  gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f);
500
501  WaitForImageLoad();
502  EXPECT_EQ(1, ImageLoadedCount());
503  // We should have default icon representation now.
504  ASSERT_EQ(1u, image.image_skia().image_reps().size());
505
506  EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f));
507
508  representation = image.image_skia().GetRepresentation(1.0f);
509  EXPECT_TRUE(gfx::BitmapsAreEqual(
510      representation.sk_bitmap(),
511      EnsureBitmapSize(
512          default_icon.GetRepresentation(1.0f).sk_bitmap(),
513          kInvalidIconSize)));
514}
515
516// Tests behavior of image created by IconImage after IconImage host goes
517// away. The image should still return loaded representations. If requested
518// representation was not loaded while IconImage host was around, transparent
519// representations should be returned.
520TEST_F(ExtensionIconImageTest, IconImageDestruction) {
521  scoped_ptr<content::BrowserContext> profile(new TestingProfile());
522  scoped_refptr<Extension> extension(CreateExtension(
523      "extension_icon_image", Manifest::INVALID_LOCATION));
524  ASSERT_TRUE(extension.get() != NULL);
525
526  gfx::ImageSkia default_icon = GetDefaultIcon();
527
528  // Load images we expect to find as representations in icon_image, so we
529  // can later use them to validate icon_image.
530  SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16);
531  ASSERT_FALSE(bitmap_16.empty());
532
533  scoped_ptr<IconImage> image(
534      new IconImage(profile.get(),
535                    extension.get(),
536                    extensions::IconsInfo::GetIcons(extension.get()),
537                    16,
538                    default_icon,
539                    this));
540
541  // Load an image representation.
542  gfx::ImageSkiaRep representation =
543      image->image_skia().GetRepresentation(1.0f);
544
545  WaitForImageLoad();
546  EXPECT_EQ(1, ImageLoadedCount());
547  ASSERT_EQ(1u, image->image_skia().image_reps().size());
548
549  // Stash loaded image skia, and destroy |image|.
550  gfx::ImageSkia image_skia = image->image_skia();
551  image.reset();
552  extension = NULL;
553
554  // Image skia should still be able to get previously loaded representation.
555  representation = image_skia.GetRepresentation(1.0f);
556
557  EXPECT_EQ(1.0f, representation.scale());
558  EXPECT_EQ(16, representation.pixel_width());
559  EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16));
560
561  // When requesting another representation, we should not crash and return some
562  // image of the size. It could be blank or a rescale from the existing 1.0f
563  // icon.
564  representation = image_skia.GetRepresentation(2.0f);
565
566  EXPECT_EQ(16, representation.GetWidth());
567  EXPECT_EQ(16, representation.GetHeight());
568  EXPECT_EQ(2.0f, representation.scale());
569}
570