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